@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/splitButton.cjs
CHANGED
|
@@ -3,5 +3,5 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
__esModule: { value: true },
|
|
4
4
|
[Symbol.toStringTag]: { value: "Module" }
|
|
5
5
|
});
|
|
6
|
-
const require_SplitButton = require("./SplitButton-
|
|
6
|
+
const require_SplitButton = require("./SplitButton-B_Y253RU.cjs");
|
|
7
7
|
exports.SplitButton = require_SplitButton.SplitButton;
|
package/splitButton.mjs
CHANGED
|
@@ -60,12 +60,13 @@ export const BottomSheetContentArea = memo<Props>(({ children, contentRef, disab
|
|
|
60
60
|
// Layout - flexible scrollable area
|
|
61
61
|
'min-h-0 flex-shrink flex-grow',
|
|
62
62
|
// When disableScroll is active (after delay), make this a flex container for children to handle their own scrolling
|
|
63
|
+
// Deliberately no focus suppression: this div has no tabIndex and is never focused
|
|
64
|
+
// programmatically, but Chrome's keyboard-focusable scroll containers can still put
|
|
65
|
+
// it in the tab order — in which case the UA ring is the only indicator it has.
|
|
63
66
|
{
|
|
64
67
|
'flex flex-col overflow-hidden': isDisableScrollActive,
|
|
65
68
|
'overflow-auto overscroll-contain': !isDisableScrollActive,
|
|
66
|
-
}
|
|
67
|
-
// Focus
|
|
68
|
-
'focus:outline-none'
|
|
69
|
+
}
|
|
69
70
|
)}
|
|
70
71
|
data-testid="rsbs-scroll"
|
|
71
72
|
key="scroll"
|
|
@@ -83,8 +83,11 @@ export const BottomSheetOverlayTranslate3d = memo<Props>(
|
|
|
83
83
|
'relative',
|
|
84
84
|
// Border radius (dynamic via CSS custom property with fallback)
|
|
85
85
|
'rounded-t-xl',
|
|
86
|
-
//
|
|
87
|
-
|
|
86
|
+
// focus-convention: intentional — this is a role="dialog" tabIndex={-1} programmatic focus
|
|
87
|
+
// target, never Tab-reachable, so suppressing the outline is correct; a ring around
|
|
88
|
+
// the whole sheet on open would be the regression. The hui- class was dead here:
|
|
89
|
+
// outline-none (utilities, later) already beat it at equal specificity (DS-98 Phase 0).
|
|
90
|
+
'focus:outline-none',
|
|
88
91
|
// EXPERIMENTAL: Static height instead of animated height
|
|
89
92
|
'h-[var(--rsbs-overlay-h-static,100vh)]',
|
|
90
93
|
// EXPERIMENTAL: translate3d for GPU acceleration
|
|
@@ -18,7 +18,7 @@ export const filledNeutralClasses = cn(
|
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
export const filledInverseClasses = cn(
|
|
21
|
-
'bg-surface-50 text-primary-500 outline-transparent
|
|
21
|
+
'hui-focus-visible-outline-inverse bg-surface-50 text-primary-500 outline-transparent hover:bg-surface-100 active:bg-surface-200 disabled:cursor-not-allowed disabled:bg-background-disabled aria-busy:cursor-not-allowed aria-busy:text-transparent'
|
|
22
22
|
);
|
|
23
23
|
|
|
24
24
|
export const outlinedNeutralClasses = cn(
|
|
@@ -33,7 +33,12 @@ export const outlinedCriticalClasses = cn(
|
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
export const outlinedInverseClasses = cn(
|
|
36
|
-
|
|
36
|
+
// Atomic (DS-98): de-prefixing the ring and deleting the bare-focus outline-color are one edit.
|
|
37
|
+
// The latter is deleted rather than de-prefixed — this string carries a static `outline outline-1
|
|
38
|
+
// outline-surface-50`, so an unconditional outline-color would permanently recolour the resting
|
|
39
|
+
// outline; it also recoloured it on mouse click, which is the SA-07 defect. The -inverse ring
|
|
40
|
+
// already sets that colour at :focus-visible.
|
|
41
|
+
'outline-solid hui-focus-visible-outline-inverse bg-transparent text-icon-inverse outline outline-1 outline-surface-50 hover:bg-background-hover-inverse active:bg-background-pressed-inverse disabled:cursor-not-allowed disabled:text-text-disabled disabled:outline-border-disabled aria-busy:cursor-not-allowed aria-busy:text-transparent'
|
|
37
42
|
);
|
|
38
43
|
|
|
39
44
|
export const transparentPrimaryClasses = cn(
|
|
@@ -49,5 +54,5 @@ export const transparentNeutralClasses = cn(
|
|
|
49
54
|
);
|
|
50
55
|
|
|
51
56
|
export const transparentInverseClasses = cn(
|
|
52
|
-
'bg-transparent text-icon-inverse outline-transparent
|
|
57
|
+
'hui-focus-visible-outline-inverse bg-transparent text-icon-inverse outline-transparent hover:bg-background-hover-inverse active:bg-background-pressed-inverse disabled:cursor-not-allowed disabled:bg-transparent aria-busy:cursor-not-allowed aria-busy:text-transparent'
|
|
53
58
|
);
|
package/src/Drawer/Drawer.tsx
CHANGED
|
@@ -372,14 +372,11 @@ export const Drawer = ({
|
|
|
372
372
|
return (
|
|
373
373
|
<>
|
|
374
374
|
<div
|
|
375
|
-
className={cn(
|
|
376
|
-
'
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
'transition-none': removeTransitionForTest,
|
|
381
|
-
}
|
|
382
|
-
)}
|
|
375
|
+
className={cn('invisible fixed inset-0 z-30 bg-background-overlay opacity-0 duration-500', {
|
|
376
|
+
'opacity-1 visible': renderDrawer,
|
|
377
|
+
'pointer-events-none invisible opacity-0': !renderDrawer,
|
|
378
|
+
'transition-none': removeTransitionForTest,
|
|
379
|
+
})}
|
|
383
380
|
data-testid="drawer-backdrop"
|
|
384
381
|
ref={backdropRef}
|
|
385
382
|
/>
|
|
@@ -44,7 +44,10 @@ export const IconOnlyFavoriteButton = forwardRef<HTMLButtonElement, IconOnlyFavo
|
|
|
44
44
|
'bg-background-primary text-critical-500': favorited,
|
|
45
45
|
},
|
|
46
46
|
// Focus state
|
|
47
|
-
|
|
47
|
+
// Atomic: the bare-focus outline suppression had to go in the same edit as the de-prefix.
|
|
48
|
+
// Prefixed,
|
|
49
|
+
// the ring was 0,3,0 and beat it; bare it is 0,2,0 and would lose (DS-98 Phase 0).
|
|
50
|
+
'hui-focus-visible-outline focus-visible:outline-offset-1',
|
|
48
51
|
// Active state - immediately show critical color when clicked
|
|
49
52
|
'active:text-border-critical-400 active:bg-critical-50',
|
|
50
53
|
// Hover state
|
|
@@ -15,7 +15,7 @@ import { useFavoriteButtonState } from '../../hooks/useFavoriteButtonState';
|
|
|
15
15
|
* - `isWiggling`: side-to-side wiggle (button), 3D twirl + bloom (heart icon),
|
|
16
16
|
* color flash (critical fill pulse on heart when not favorited), and shimmer (label text).
|
|
17
17
|
* During nudge the heart uses `fill` weight with critical color; at rest unfavorited uses `bold`.
|
|
18
|
-
* Apply for ~
|
|
18
|
+
* Apply for ~650ms, then reset to false to re-arm.
|
|
19
19
|
*
|
|
20
20
|
* - `isActivating`: pulse (button) and heart pop (icon). Apply for ~500ms.
|
|
21
21
|
*
|
|
@@ -25,6 +25,13 @@ import { useFavoriteButtonState } from '../../hooks/useFavoriteButtonState';
|
|
|
25
25
|
* The heart-color flash uses `.hui-fb-color-flash` to animate fill on the nested
|
|
26
26
|
* SVG path via a descendant selector. Icon weight: `bold` when unfavorited at rest,
|
|
27
27
|
* `fill` during nudge, favorited, or active press — matching the static `labeled` variant at rest.
|
|
28
|
+
*
|
|
29
|
+
* Reduced motion is gated once at the registry level
|
|
30
|
+
* (`theme-system/src/tailwind/reducedMotion.ts`), so these call sites deliberately carry no
|
|
31
|
+
* `motion-reduce:` variants — adding them back would reintroduce a second mechanism. The
|
|
32
|
+
* shimmer is the one exception: `.hui-fb-shimmer` also drops its gradient under reduce, because
|
|
33
|
+
* the label is painted *by* that gradient (`-webkit-text-fill-color: transparent`) and would
|
|
34
|
+
* otherwise be illegible.
|
|
28
35
|
*/
|
|
29
36
|
|
|
30
37
|
type Props = {
|
|
@@ -42,7 +49,7 @@ type Props = {
|
|
|
42
49
|
isActivating?: boolean;
|
|
43
50
|
/**
|
|
44
51
|
* When true, fires the nudge animation (wiggle + twirl + shimmer + color flash).
|
|
45
|
-
* Reset to false after ~
|
|
52
|
+
* Reset to false after ~650ms to re-arm.
|
|
46
53
|
*/
|
|
47
54
|
isWiggling?: boolean;
|
|
48
55
|
/** Label text shown when not favorited. @default 'Save' */
|
|
@@ -122,10 +129,10 @@ export const LabeledAnimatedFavoriteButton = forwardRef<HTMLButtonElement, Props
|
|
|
122
129
|
'hover:border-critical-700 hover:bg-critical-50 hover:text-critical-700',
|
|
123
130
|
'active:border-critical-900 active:bg-critical-100 active:text-critical-900',
|
|
124
131
|
// Nudge animation — wiggle on button root + color flash on nested SVG path
|
|
125
|
-
isWiggling && 'animate-saveButtonWiggle
|
|
132
|
+
isWiggling && 'animate-saveButtonWiggle',
|
|
126
133
|
isWiggling && !favorited && 'hui-fb-color-flash',
|
|
127
134
|
// Save-click animation — pulse on button root
|
|
128
|
-
isActivating && 'animate-saveButtonPulse
|
|
135
|
+
isActivating && 'animate-saveButtonPulse',
|
|
129
136
|
className
|
|
130
137
|
)}
|
|
131
138
|
data-hui-id="FavoriteButton"
|
|
@@ -148,7 +155,7 @@ export const LabeledAnimatedFavoriteButton = forwardRef<HTMLButtonElement, Props
|
|
|
148
155
|
// Critical color during nudge so fill weight reads red immediately (not surface-500 grey)
|
|
149
156
|
isWiggling && !favorited && 'text-critical-500',
|
|
150
157
|
// Nudge animation — 3D twirl + bloom on icon frame
|
|
151
|
-
isWiggling && 'animate-saveHeartTwirlBloom
|
|
158
|
+
isWiggling && 'animate-saveHeartTwirlBloom'
|
|
152
159
|
)}
|
|
153
160
|
style={{ height: '14px', perspective: '200px', width: '14px' }}
|
|
154
161
|
>
|
|
@@ -156,7 +163,7 @@ export const LabeledAnimatedFavoriteButton = forwardRef<HTMLButtonElement, Props
|
|
|
156
163
|
className={cn(
|
|
157
164
|
'[transform-style:preserve-3d]',
|
|
158
165
|
// Save-click animation — heart pop on SVG element
|
|
159
|
-
isActivating && 'animate-saveHeartPop
|
|
166
|
+
isActivating && 'animate-saveHeartPop'
|
|
160
167
|
)}
|
|
161
168
|
color="inherit"
|
|
162
169
|
size="xs"
|
|
@@ -63,7 +63,7 @@ export type FavoriteButtonProps = BaseFavoriteButtonProps & {
|
|
|
63
63
|
/**
|
|
64
64
|
* When true, fires the nudge animation (wiggle + twirl + shimmer).
|
|
65
65
|
* Only meaningful for the `labeled-animated` variant.
|
|
66
|
-
* Reset to false after ~
|
|
66
|
+
* Reset to false after ~650ms to re-arm.
|
|
67
67
|
*/
|
|
68
68
|
isWiggling?: boolean;
|
|
69
69
|
/**
|
|
@@ -230,7 +230,7 @@ export const GridFileCard = ({
|
|
|
230
230
|
// transitions
|
|
231
231
|
'opacity-0 transition-opacity duration-200',
|
|
232
232
|
// group focus/hover states
|
|
233
|
-
'group-
|
|
233
|
+
'group-hover/grid-file-card:opacity-100 group-has-[:focus-visible]/grid-file-card:opacity-100',
|
|
234
234
|
// mouse pointer
|
|
235
235
|
'cursor-pointer',
|
|
236
236
|
{
|
|
@@ -78,8 +78,8 @@ export const ListFileCard = ({
|
|
|
78
78
|
|
|
79
79
|
const actionBtnClasses = showActionsOnHover
|
|
80
80
|
? cn(
|
|
81
|
-
'opacity-0 group-
|
|
82
|
-
'focus:opacity-100',
|
|
81
|
+
'opacity-0 group-hover/list-file-card:opacity-100 group-has-[:focus-visible]/list-file-card:opacity-100',
|
|
82
|
+
'focus-visible:opacity-100',
|
|
83
83
|
'transition-opacity duration-200'
|
|
84
84
|
)
|
|
85
85
|
: '';
|
|
@@ -382,7 +382,9 @@ export const ImageSlider = ({
|
|
|
382
382
|
aria-label="Image gallery"
|
|
383
383
|
aria-roledescription="carousel"
|
|
384
384
|
className={cn(
|
|
385
|
-
|
|
385
|
+
// Adds the shared ring — this role="application" container is tabIndex={0} and
|
|
386
|
+
// was shipping with no focus indicator at all (WCAG 2.4.7) — DS-98 row 17.
|
|
387
|
+
'hui-focus-visible-outline relative mx-auto w-full pr-lg smMax:pr-0',
|
|
386
388
|
// Only extend beyond viewport for bleed effect when there are multiple cards
|
|
387
389
|
{ 'xsMax:-ml-sm xsMax:w-[calc(100%+32px)]': numberOfPages > 1 },
|
|
388
390
|
className
|
|
@@ -129,7 +129,7 @@ export const Thumbnails = ({
|
|
|
129
129
|
aria-selected={isActive}
|
|
130
130
|
className={cn(
|
|
131
131
|
'box-border flex-shrink-0 cursor-pointer overflow-hidden rounded-md transition-all duration-100',
|
|
132
|
-
'hui-focus-visible-outline hover:opacity-80 focus:-outline-offset-4',
|
|
132
|
+
'hui-focus-visible-outline hover:opacity-80 focus-visible:-outline-offset-4',
|
|
133
133
|
`aspect-square h-[${thumbnailSize}px] w-[${thumbnailSize}px] max-h-[${thumbnailSize}px] max-w-[${thumbnailSize}px]`,
|
|
134
134
|
// Use outline instead of border to avoid affecting box dimensions
|
|
135
135
|
isActive
|
|
@@ -35,7 +35,10 @@ export const JumpMenuItem = ({
|
|
|
35
35
|
'hui-text-body-secondary relative whitespace-nowrap rounded-md px-xs py-2xs last:mr-sm [&:not(:last-child)]:mr-xs',
|
|
36
36
|
// Default state (Selected=Off, Hover=Off, Focused=Off)
|
|
37
37
|
'text-text-secondary',
|
|
38
|
-
// Focus styles - inset focus ring to stay within button dimensions
|
|
38
|
+
// Focus styles - inset focus ring to stay within button dimensions.
|
|
39
|
+
// Permitted clause-2 deviation (DS-98): hui-focus-visible-outline is outline-based and
|
|
40
|
+
// cannot express an inset ring. The outline-none is focus-visible-scoped, so it
|
|
41
|
+
// suppresses only alongside this replacement indicator.
|
|
39
42
|
'focus-visible:shadow-[inset_0_0_0_4px_rgb(var(--color-focus-primary)/0.50)] focus-visible:outline-none',
|
|
40
43
|
// Interactive states
|
|
41
44
|
'cursor-pointer',
|
|
@@ -215,7 +215,7 @@ export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps & HTMLAttribute
|
|
|
215
215
|
className={cn(
|
|
216
216
|
{
|
|
217
217
|
'active:bg-background-hover': !isHeader,
|
|
218
|
-
'focus:bg-background-hover': !isHeader,
|
|
218
|
+
'focus-visible:bg-background-hover': !isHeader,
|
|
219
219
|
'hover:bg-background-hover': !isHeader,
|
|
220
220
|
},
|
|
221
221
|
'MenuItem relative grid w-full cursor-pointer items-center justify-start gap-x-xs border-none px-sm py-3 outline-0',
|
|
@@ -238,7 +238,15 @@ export const ComposableModal = ({
|
|
|
238
238
|
)}
|
|
239
239
|
data-hui-id="Modal"
|
|
240
240
|
data-testid={dataTestId}
|
|
241
|
-
onAnimationEnd={() => {
|
|
241
|
+
onAnimationEnd={(event) => {
|
|
242
|
+
// `animationend` bubbles, so only the dialog's own close animation counts.
|
|
243
|
+
// Under `prefers-reduced-motion: reduce` every animation collapses to ~1ms,
|
|
244
|
+
// which compresses descendant completions into the same tick as the close —
|
|
245
|
+
// without this check an animated child could close the modal early.
|
|
246
|
+
if (event.target !== event.currentTarget) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
242
250
|
if (modalAnimation === 'closing') {
|
|
243
251
|
onModalCloseAnimationEnd?.();
|
|
244
252
|
}
|
|
@@ -120,7 +120,14 @@ export const useCloseModal = (handleCloseData?: any) => {
|
|
|
120
120
|
|
|
121
121
|
setModalAnimation('closing');
|
|
122
122
|
|
|
123
|
-
container.addEventListener('animationend', () => {
|
|
123
|
+
container.addEventListener('animationend', (event) => {
|
|
124
|
+
// `animationend` bubbles up from descendants. Under `prefers-reduced-motion: reduce`
|
|
125
|
+
// every animation collapses to ~1ms, so a child finishing in the same tick would
|
|
126
|
+
// otherwise clear the watchdog and resolve the close before the modal has animated.
|
|
127
|
+
if (event.target !== event.currentTarget) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
clearTimeout(timeout);
|
|
125
132
|
forceCloseModal();
|
|
126
133
|
storedResolve();
|
|
@@ -306,6 +306,12 @@ export const PhoneInput = (unfilteredProps: Props) => {
|
|
|
306
306
|
}, [countryMenuOpen, usePortalMenu]);
|
|
307
307
|
|
|
308
308
|
// Create the country dropdown content
|
|
309
|
+
// focus-convention: intentional — text field resting border (:focus-visible always matches a
|
|
310
|
+
// focused text input, mouse or keyboard). The outline-none is permitted because the border
|
|
311
|
+
// change is itself a visible focus indicator, so WCAG 2.4.7 is still met (SA-07/DS-98).
|
|
312
|
+
const countrySearchClasses =
|
|
313
|
+
'rounded w-full border border-border-secondary bg-background-primary px-xs py-2xs !text-[16px] text-sm text-text-primary placeholder:text-[16px] placeholder:text-text-inactive focus:border-border-selected focus:outline-none';
|
|
314
|
+
|
|
309
315
|
const countryDropdownContent = (
|
|
310
316
|
<div
|
|
311
317
|
aria-label="Country list"
|
|
@@ -341,7 +347,7 @@ export const PhoneInput = (unfilteredProps: Props) => {
|
|
|
341
347
|
<div className="px-xs pb-xs">
|
|
342
348
|
<input
|
|
343
349
|
aria-label="Search countries"
|
|
344
|
-
className=
|
|
350
|
+
className={countrySearchClasses}
|
|
345
351
|
onBlur={(e) => {
|
|
346
352
|
e.stopPropagation();
|
|
347
353
|
e.preventDefault();
|
|
@@ -229,7 +229,7 @@ export const ProgressCircle = ({
|
|
|
229
229
|
x={iconViewBoxOffset}
|
|
230
230
|
y={iconViewBoxOffset}
|
|
231
231
|
>
|
|
232
|
-
<div className="flex h-full w-full
|
|
232
|
+
<div className="animate-progress-icon-fade-in flex h-full w-full items-center justify-center">
|
|
233
233
|
<CheckCircle
|
|
234
234
|
color="success"
|
|
235
235
|
size="md"
|
|
@@ -168,7 +168,7 @@ export const SearchInput = ({
|
|
|
168
168
|
<div className={cn('rounded-search-input bg-background-primary', className)}>
|
|
169
169
|
<form
|
|
170
170
|
className={cn(
|
|
171
|
-
'overflow-none box-content flex items-center gap-2xs rounded-search-input border border-border-secondary bg-background-primary outline-1 outline-background-critical-dark hover:border-border-hover has-[:focus]:border-border-selected',
|
|
171
|
+
'overflow-none box-content flex items-center gap-2xs rounded-search-input border border-border-secondary bg-background-primary outline-1 outline-background-critical-dark hover:border-border-hover has-[:focus-visible]:border-border-selected',
|
|
172
172
|
{
|
|
173
173
|
'border-0': emphasis === 'bold',
|
|
174
174
|
'h-[32px]': small, // Smaller due to box-content to prevent clipping
|
|
@@ -184,11 +184,12 @@ export const SearchInput = ({
|
|
|
184
184
|
},
|
|
185
185
|
// Focus Styles
|
|
186
186
|
{
|
|
187
|
-
'has-[:focus]:border-border-secondary has-[:focus]:bg-background-primary has-[:focus]:outline has-[:focus]:outline-focus-inverse':
|
|
187
|
+
'has-[:focus-visible]:border-border-secondary has-[:focus-visible]:bg-background-primary has-[:focus-visible]:outline has-[:focus-visible]:outline-focus-inverse':
|
|
188
188
|
emphasis === 'bold',
|
|
189
|
-
'has-[:focus]:outline-2 has-[:focus]:outline-border-black':
|
|
189
|
+
'has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-border-black':
|
|
190
190
|
style === 'neutral' && emphasis === 'bold',
|
|
191
|
-
'has-[:focus]:outline-4 has-[:focus]:outline-focus-inverse':
|
|
191
|
+
'has-[:focus-visible]:outline-4 has-[:focus-visible]:outline-focus-inverse':
|
|
192
|
+
style === 'inverse',
|
|
192
193
|
}
|
|
193
194
|
)}
|
|
194
195
|
data-hui-id="SearchInput"
|
|
@@ -142,8 +142,7 @@ export const Option = ({
|
|
|
142
142
|
// Selected AND disabled - show as selected but with disabled text color
|
|
143
143
|
'!cursor-not-allowed !bg-background-inverse !text-text-disabled hover:!bg-background-inverse':
|
|
144
144
|
isSelectedAndDisabled,
|
|
145
|
-
'!cursor-not-allowed !text-text-disabled hover:!bg-background-primary
|
|
146
|
-
isDisabled && !isSelected,
|
|
145
|
+
'!cursor-not-allowed !text-text-disabled hover:!bg-background-primary': isDisabled && !isSelected,
|
|
147
146
|
}
|
|
148
147
|
)}
|
|
149
148
|
innerProps={{
|
package/src/Tabs/Tabs.tsx
CHANGED
|
@@ -105,8 +105,9 @@ export const Tabs = ({
|
|
|
105
105
|
'hui-text-h5',
|
|
106
106
|
// Base styles
|
|
107
107
|
'z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none',
|
|
108
|
-
// Focus visible styles
|
|
109
|
-
|
|
108
|
+
// Focus visible styles. The offset is load-bearing: the base `outline-none` above
|
|
109
|
+
// sets outline-offset: 2px, and hui-focus-visible-outline sets no offset.
|
|
110
|
+
'hui-focus-visible-outline focus-visible:-outline-offset-1',
|
|
110
111
|
// Base state
|
|
111
112
|
'cursor-pointer text-text-secondary',
|
|
112
113
|
|
|
@@ -122,8 +123,7 @@ export const Tabs = ({
|
|
|
122
123
|
},
|
|
123
124
|
// Active tab styles
|
|
124
125
|
{
|
|
125
|
-
'border-b-border-selected text-text-primary
|
|
126
|
-
tab.contentId === activeTab,
|
|
126
|
+
'border-b-border-selected text-text-primary': tab.contentId === activeTab,
|
|
127
127
|
}
|
|
128
128
|
)}
|
|
129
129
|
key={tab.contentId}
|
|
@@ -131,6 +131,8 @@ export const TextAreaInput = forwardRef<HTMLTextAreaElement, Props>((unfilteredP
|
|
|
131
131
|
'hover:border-border-hover': !formLabelProps.disabled,
|
|
132
132
|
},
|
|
133
133
|
// Focus state (when not disabled)
|
|
134
|
+
// focus-convention: intentional — text field resting border; :focus-visible always matches a
|
|
135
|
+
// focused text input, and the border is expected on mouse focus too (SA-07/DS-98).
|
|
134
136
|
{
|
|
135
137
|
'focus:border-border-focused': !formLabelProps.disabled,
|
|
136
138
|
},
|
|
@@ -105,7 +105,7 @@ export const TextInput = forwardRef<HTMLInputElement, Props>((unfilteredProps, r
|
|
|
105
105
|
>
|
|
106
106
|
<div
|
|
107
107
|
className={cn(
|
|
108
|
-
'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',
|
|
108
|
+
'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',
|
|
109
109
|
{
|
|
110
110
|
'!border-border-disabled bg-background-disabled text-text-disabled': disabled,
|
|
111
111
|
'border-border-critical': hasError,
|
|
@@ -29,7 +29,7 @@ export const InfoIconTooltip = ({
|
|
|
29
29
|
<>
|
|
30
30
|
<Info
|
|
31
31
|
aria-label={ariaLabel}
|
|
32
|
-
className={cn('
|
|
32
|
+
className={cn('hui-focus-visible-outline overflow-visible', className)}
|
|
33
33
|
color={color}
|
|
34
34
|
id={id}
|
|
35
35
|
role="button"
|
|
@@ -112,10 +112,11 @@ export const InlineButton = forwardRef<HTMLButtonElement, Props>(
|
|
|
112
112
|
className={cn(
|
|
113
113
|
// GENERAL
|
|
114
114
|
'hui-text-body-primary inline-flex w-min cursor-pointer items-center gap-2xs whitespace-nowrap border-none bg-transparent p-0 tracking-[0.5px] outline-none',
|
|
115
|
-
// FOCUS
|
|
116
|
-
|
|
115
|
+
// FOCUS — the base `outline-none` above is unvariant (0,1,0), so the bare ring
|
|
116
|
+
// (0,2,0) still wins after de-prefixing (DS-98 Phase 0).
|
|
117
|
+
'hui-focus-visible-outline focus-visible:underline',
|
|
117
118
|
{
|
|
118
|
-
'
|
|
119
|
+
'hui-focus-visible-outline-inverse': style === 'inverse',
|
|
119
120
|
},
|
|
120
121
|
// HOVER
|
|
121
122
|
'hover:underline',
|
|
@@ -13,6 +13,15 @@ const easeInOutCubic = (t: number): number => {
|
|
|
13
13
|
* @param targetY - The target vertical scroll position in pixels
|
|
14
14
|
* @param duration - The duration of the scroll animation in milliseconds (defaults to 300ms)
|
|
15
15
|
*
|
|
16
|
+
* Under `prefers-reduced-motion: reduce` this jumps straight to the target instead of easing.
|
|
17
|
+
* The preference is read once, at call time, so toggling it mid-scroll will not interrupt a
|
|
18
|
+
* scroll already in flight — deliberate, since the scroll is at most a few hundred milliseconds.
|
|
19
|
+
*
|
|
20
|
+
* The `typeof` check is required rather than defensive: jsdom does not implement `matchMedia`,
|
|
21
|
+
* so an unguarded call would throw in tests. (`window` itself is not guarded because this
|
|
22
|
+
* function already dereferences `window.scrollY` unconditionally and only runs from event
|
|
23
|
+
* handlers.)
|
|
24
|
+
*
|
|
16
25
|
* @example
|
|
17
26
|
* ```tsx
|
|
18
27
|
* // Scroll to 500px from the top over 300ms
|
|
@@ -27,6 +36,15 @@ const easeInOutCubic = (t: number): number => {
|
|
|
27
36
|
* ```
|
|
28
37
|
*/
|
|
29
38
|
export const smoothScrollTo = (targetY: number, duration: number = 300): void => {
|
|
39
|
+
const prefersReducedMotion =
|
|
40
|
+
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
41
|
+
|
|
42
|
+
if (prefersReducedMotion) {
|
|
43
|
+
window.scrollTo(0, targetY);
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
const startY = window.scrollY;
|
|
31
49
|
const distance = targetY - startY;
|
|
32
50
|
const startTime = performance.now();
|
package/table.cjs
CHANGED
package/table.mjs
CHANGED
package/tabs.cjs
CHANGED
|
@@ -42,7 +42,7 @@ const Tabs = ({ className, contentClassName, currentTab, defaultTab, isContentSc
|
|
|
42
42
|
}, [isContentScrollable, checkScrollState]);
|
|
43
43
|
const renderedTabs = tabs.map((tab) => {
|
|
44
44
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
45
|
-
className: (0, _evolve_ui_theme_system.cn)("hui-text-h5", "z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none", "hui-focus-visible-outline focus
|
|
45
|
+
className: (0, _evolve_ui_theme_system.cn)("hui-text-h5", "z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none", "hui-focus-visible-outline focus-visible:-outline-offset-1", "cursor-pointer text-text-secondary", { "hover:border-b-border-hover hover:text-link-hover": tab.contentId !== activeTab }, { "mb-2xs": isContentScrollable }, { "active:border-b-border-selected active:text-text-primary": tab.contentId !== activeTab }, { "border-b-border-selected text-text-primary": tab.contentId === activeTab }),
|
|
46
46
|
onClick: (e) => {
|
|
47
47
|
if (typeof e.currentTarget.scrollIntoView === "function") e.currentTarget.scrollIntoView({
|
|
48
48
|
behavior: "smooth",
|
package/tabs.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.cjs","names":["useHorizontalScrollFadeMixin"],"sources":["../src/Tabs/Tabs.tsx"],"sourcesContent":["import { cn, wrapVarRgb } from '@evolve.ui/theme-system';\nimport { ReactNode, useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\n\nexport type Tab = {\n /** The content to render when this tab is selected */\n content: ReactNode;\n /** The id of the content to render when this tab is selected */\n contentId: string;\n /** The label of the tab */\n label: string | ReactNode;\n /** The function to call when the tab is clicked */\n onClick?: (contentId: string) => void;\n};\n\ntype Props = {\n className?: string;\n contentClassName?: string;\n isContentScrollable?: boolean;\n /**\n * When `true` (default), inactive tab panels stay mounted in the DOM but are visually hidden\n * and marked `inert` so their content is not keyboard- or assistive-technology-interactive.\n */\n keepTabsInDOM?: boolean;\n tabs: Tab[];\n} & (\n | {\n /** Used with `onTabChanged` to control tabs externally */\n currentTab: string;\n /** Use only if `currentTab` and `onTabChanged` are not provided */\n defaultTab?: never;\n /** Used with `currentTab` to control tabs externally */\n onTabChanged: (contentId: string) => void;\n }\n | {\n /** Use only if `defaultTab` is not provided. This is the contentId of the tab to be selected */\n currentTab?: never;\n /** Sets the default tab on first render. This is the contentId of the tab to be selected */\n defaultTab?: string;\n /** Use only if `defaultTab` is not provided */\n onTabChanged?: never;\n }\n);\n\nexport const Tabs = ({\n className,\n contentClassName,\n currentTab,\n defaultTab,\n isContentScrollable = false,\n keepTabsInDOM = true,\n onTabChanged,\n tabs,\n}: Props) => {\n const [selectedTab, setSelectedTab] = useState(defaultTab ?? currentTab ?? tabs[0].contentId);\n // In controlled mode, use currentTab as the source of truth; otherwise use selectedTab\n const activeTab = currentTab !== undefined ? currentTab : selectedTab;\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n\n // Create refs for each tab panel to handle scrolling\n const tabPanelRefs = useRef<Record<string, HTMLDivElement | null>>({});\n // Create refs for each tab button to handle scrolling into view\n const tabButtonRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n\n // Track scroll state for gradient overlay\n // Start with true if content is scrollable, will be updated once we measure the actual content\n const [isScrollable, setIsScrollable] = useState(isContentScrollable);\n const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);\n\n /**\n * Checks if the content is scrollable and if it's scrolled to the bottom\n * @param element - The scrollable element to check\n */\n const checkScrollState = useCallback((element: HTMLDivElement) => {\n const { clientHeight, scrollHeight, scrollTop } = element;\n const scrollable = scrollHeight > clientHeight;\n const scrolledToBottom = scrollable ? scrollHeight - scrollTop <= clientHeight + 1 : true;\n\n setIsScrollable(scrollable);\n setIsScrolledToBottom(scrolledToBottom);\n }, []);\n\n /**\n * Scrolls the content area to the top if isContentScrollable is true\n * @param contentId - The content ID of the tab panel to scroll\n */\n const scrollToTop = useCallback(\n (contentId: string) => {\n if (isContentScrollable) {\n const tabPanel = tabPanelRefs.current[contentId];\n if (tabPanel) {\n tabPanel.scrollTop = 0;\n checkScrollState(tabPanel);\n }\n }\n },\n [isContentScrollable, checkScrollState]\n );\n\n const renderedTabs = tabs.map((tab) => {\n return (\n <button\n className={cn(\n 'hui-text-h5',\n // Base styles\n 'z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none',\n // Focus visible styles\n 'hui-focus-visible-outline focus:text-text-secondary focus:-outline-offset-1',\n // Base state\n 'cursor-pointer text-text-secondary',\n\n // Hover state (when not active)\n {\n 'hover:border-b-border-hover hover:text-link-hover': tab.contentId !== activeTab,\n },\n // Spacing adjustments - mb to create space between border and divider line\n { 'mb-2xs': isContentScrollable },\n // Active state (when not disabled)\n {\n 'active:border-b-border-selected active:text-text-primary': tab.contentId !== activeTab,\n },\n // Active tab styles\n {\n 'border-b-border-selected text-text-primary focus:text-text-primary':\n tab.contentId === activeTab,\n }\n )}\n key={tab.contentId}\n onClick={(e) => {\n // Scroll the clicked tab into view FIRST, before state updates\n if (typeof e.currentTarget.scrollIntoView === 'function') {\n e.currentTarget.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n });\n }\n\n setSelectedTab(tab.contentId);\n scrollToTop(tab.contentId);\n onTabChanged?.(tab.contentId);\n if (tab.onClick) {\n tab.onClick(tab.contentId);\n }\n }}\n ref={(el) => {\n tabButtonRefs.current[tab.contentId] = el;\n }}\n aria-selected={tab.contentId === activeTab}\n role=\"tab\"\n >\n {tab.label}\n </button>\n );\n });\n\n const renderedTabPanels = tabs.map((tab) => {\n const isActive = tab.contentId === activeTab;\n const isScrollable = isContentScrollable && isActive;\n\n const props = {\n 'aria-labelledby': tab.contentId,\n id: tab.contentId,\n role: 'tabpanel',\n // Scrollable regions must be focusable for keyboard accessibility\n ...(isScrollable && { tabIndex: 0 }),\n };\n\n const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {\n if (isContentScrollable && isActive) {\n checkScrollState(e.currentTarget);\n }\n };\n\n if (keepTabsInDOM) {\n return (\n <div\n className={cn(\n {\n 'absolute h-0 w-0 overflow-hidden': !isActive,\n 'h-full overflow-y-auto': isScrollable,\n },\n contentClassName\n )}\n inert={!isActive}\n key={tab.contentId}\n onScroll={handleScroll}\n ref={(el) => {\n tabPanelRefs.current[tab.contentId] = el;\n if (el && isActive && isContentScrollable) {\n checkScrollState(el);\n }\n }}\n {...props}\n >\n {tab.content}\n </div>\n );\n }\n\n if (!isActive) {\n return null;\n }\n\n return (\n <div\n className={cn(\n {\n 'h-full overflow-y-auto': isContentScrollable,\n },\n contentClassName\n )}\n key={tab.contentId}\n onScroll={handleScroll}\n ref={(el) => {\n tabPanelRefs.current[tab.contentId] = el;\n if (el && isContentScrollable) {\n checkScrollState(el);\n }\n }}\n {...props}\n >\n {tab.content}\n </div>\n );\n });\n // Handle controlled tabs - when currentTab prop changes externally, update internal state and scroll tab into view\n useEffect(() => {\n if (currentTab && currentTab !== selectedTab) {\n setSelectedTab(currentTab);\n scrollToTop(currentTab);\n\n // Scroll the tab button into view when controlled externally\n const tabButton = tabButtonRefs.current[currentTab];\n if (tabButton && typeof tabButton.scrollIntoView === 'function') {\n tabButton.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n });\n }\n }\n }, [currentTab, selectedTab, scrollToTop]);\n\n // Scroll the selected tab into view on initial render\n // Only scroll if it's not the first tab (first tab is already visible)\n // This is useful for bottom sheet on mobile where a non-first tab might be selected initially\n useEffect(() => {\n // Skip scroll if it's the first tab to prevent page jumping on initial load\n if (selectedTab === tabs[0].contentId) {\n return;\n }\n\n const tabButton = tabButtonRefs.current[selectedTab];\n if (tabButton && typeof tabButton.scrollIntoView === 'function') {\n tabButton.scrollIntoView({\n behavior: 'instant',\n block: 'nearest',\n inline: 'center',\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Only run on mount\n\n // Check scroll state when active tab changes or content loads\n useEffect(() => {\n if (isContentScrollable) {\n const activePanel = tabPanelRefs.current[activeTab];\n if (activePanel) {\n // Match BottomSheet opening animation duration to ensure layout is stable\n const timer = setTimeout(() => {\n checkScrollState(activePanel);\n }, 300);\n\n return () => clearTimeout(timer);\n }\n }\n\n return undefined;\n }, [activeTab, isContentScrollable, checkScrollState, tabs]);\n\n return (\n <div\n className={cn(className, {\n 'flex h-full min-h-0 flex-1 flex-col': isContentScrollable,\n })}\n >\n <div\n className=\"relative flex-shrink-0 overflow-x-clip\"\n role=\"tablist\"\n >\n <div\n className=\"flex gap-sm overflow-x-auto xsMax:scrollbar-hide\"\n ref={horizontalScrollFadeProps.ref}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute left-0 top-0 z-10 w-[48px] bg-gradient-to-r from-background-primary to-transparent transition-opacity duration-300 ease-in-out',\n {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute right-0 top-0 z-10 w-[48px] bg-gradient-to-l from-background-primary to-transparent transition-opacity duration-300 ease-in-out',\n {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n />\n\n {renderedTabs}\n <div\n className={cn('absolute left-0 right-0 z-[1] h-px border-b border-border-secondary', {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n })}\n role=\"none\"\n />\n </div>\n </div>\n <div\n className={cn('relative', {\n 'min-h-0 flex-grow pb-3xs': isContentScrollable,\n })}\n >\n {renderedTabPanels}\n {/* Bottom gradient overlay - shows when content is scrollable and not at bottom */}\n {isContentScrollable && (\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 right-0 h-xl transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': isScrolledToBottom && isScrollable,\n 'opacity-100': !isScrolledToBottom && isScrollable,\n }\n )}\n data-testid=\"tabs-gradient-overlay\"\n style={{\n background: `linear-gradient(to bottom, transparent, ${wrapVarRgb('--color-background-primary')})`,\n }}\n />\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabs.cjs","names":["useHorizontalScrollFadeMixin"],"sources":["../src/Tabs/Tabs.tsx"],"sourcesContent":["import { cn, wrapVarRgb } from '@evolve.ui/theme-system';\nimport { ReactNode, useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useHorizontalScrollFadeMixin } from '../mixins/horizontalScrollFadeMixin';\n\nexport type Tab = {\n /** The content to render when this tab is selected */\n content: ReactNode;\n /** The id of the content to render when this tab is selected */\n contentId: string;\n /** The label of the tab */\n label: string | ReactNode;\n /** The function to call when the tab is clicked */\n onClick?: (contentId: string) => void;\n};\n\ntype Props = {\n className?: string;\n contentClassName?: string;\n isContentScrollable?: boolean;\n /**\n * When `true` (default), inactive tab panels stay mounted in the DOM but are visually hidden\n * and marked `inert` so their content is not keyboard- or assistive-technology-interactive.\n */\n keepTabsInDOM?: boolean;\n tabs: Tab[];\n} & (\n | {\n /** Used with `onTabChanged` to control tabs externally */\n currentTab: string;\n /** Use only if `currentTab` and `onTabChanged` are not provided */\n defaultTab?: never;\n /** Used with `currentTab` to control tabs externally */\n onTabChanged: (contentId: string) => void;\n }\n | {\n /** Use only if `defaultTab` is not provided. This is the contentId of the tab to be selected */\n currentTab?: never;\n /** Sets the default tab on first render. This is the contentId of the tab to be selected */\n defaultTab?: string;\n /** Use only if `defaultTab` is not provided */\n onTabChanged?: never;\n }\n);\n\nexport const Tabs = ({\n className,\n contentClassName,\n currentTab,\n defaultTab,\n isContentScrollable = false,\n keepTabsInDOM = true,\n onTabChanged,\n tabs,\n}: Props) => {\n const [selectedTab, setSelectedTab] = useState(defaultTab ?? currentTab ?? tabs[0].contentId);\n // In controlled mode, use currentTab as the source of truth; otherwise use selectedTab\n const activeTab = currentTab !== undefined ? currentTab : selectedTab;\n const horizontalScrollFadeProps = useHorizontalScrollFadeMixin(0);\n\n // Create refs for each tab panel to handle scrolling\n const tabPanelRefs = useRef<Record<string, HTMLDivElement | null>>({});\n // Create refs for each tab button to handle scrolling into view\n const tabButtonRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n\n // Track scroll state for gradient overlay\n // Start with true if content is scrollable, will be updated once we measure the actual content\n const [isScrollable, setIsScrollable] = useState(isContentScrollable);\n const [isScrolledToBottom, setIsScrolledToBottom] = useState(false);\n\n /**\n * Checks if the content is scrollable and if it's scrolled to the bottom\n * @param element - The scrollable element to check\n */\n const checkScrollState = useCallback((element: HTMLDivElement) => {\n const { clientHeight, scrollHeight, scrollTop } = element;\n const scrollable = scrollHeight > clientHeight;\n const scrolledToBottom = scrollable ? scrollHeight - scrollTop <= clientHeight + 1 : true;\n\n setIsScrollable(scrollable);\n setIsScrolledToBottom(scrolledToBottom);\n }, []);\n\n /**\n * Scrolls the content area to the top if isContentScrollable is true\n * @param contentId - The content ID of the tab panel to scroll\n */\n const scrollToTop = useCallback(\n (contentId: string) => {\n if (isContentScrollable) {\n const tabPanel = tabPanelRefs.current[contentId];\n if (tabPanel) {\n tabPanel.scrollTop = 0;\n checkScrollState(tabPanel);\n }\n }\n },\n [isContentScrollable, checkScrollState]\n );\n\n const renderedTabs = tabs.map((tab) => {\n return (\n <button\n className={cn(\n 'hui-text-h5',\n // Base styles\n 'z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none',\n // Focus visible styles. The offset is load-bearing: the base `outline-none` above\n // sets outline-offset: 2px, and hui-focus-visible-outline sets no offset.\n 'hui-focus-visible-outline focus-visible:-outline-offset-1',\n // Base state\n 'cursor-pointer text-text-secondary',\n\n // Hover state (when not active)\n {\n 'hover:border-b-border-hover hover:text-link-hover': tab.contentId !== activeTab,\n },\n // Spacing adjustments - mb to create space between border and divider line\n { 'mb-2xs': isContentScrollable },\n // Active state (when not disabled)\n {\n 'active:border-b-border-selected active:text-text-primary': tab.contentId !== activeTab,\n },\n // Active tab styles\n {\n 'border-b-border-selected text-text-primary': tab.contentId === activeTab,\n }\n )}\n key={tab.contentId}\n onClick={(e) => {\n // Scroll the clicked tab into view FIRST, before state updates\n if (typeof e.currentTarget.scrollIntoView === 'function') {\n e.currentTarget.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n });\n }\n\n setSelectedTab(tab.contentId);\n scrollToTop(tab.contentId);\n onTabChanged?.(tab.contentId);\n if (tab.onClick) {\n tab.onClick(tab.contentId);\n }\n }}\n ref={(el) => {\n tabButtonRefs.current[tab.contentId] = el;\n }}\n aria-selected={tab.contentId === activeTab}\n role=\"tab\"\n >\n {tab.label}\n </button>\n );\n });\n\n const renderedTabPanels = tabs.map((tab) => {\n const isActive = tab.contentId === activeTab;\n const isScrollable = isContentScrollable && isActive;\n\n const props = {\n 'aria-labelledby': tab.contentId,\n id: tab.contentId,\n role: 'tabpanel',\n // Scrollable regions must be focusable for keyboard accessibility\n ...(isScrollable && { tabIndex: 0 }),\n };\n\n const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {\n if (isContentScrollable && isActive) {\n checkScrollState(e.currentTarget);\n }\n };\n\n if (keepTabsInDOM) {\n return (\n <div\n className={cn(\n {\n 'absolute h-0 w-0 overflow-hidden': !isActive,\n 'h-full overflow-y-auto': isScrollable,\n },\n contentClassName\n )}\n inert={!isActive}\n key={tab.contentId}\n onScroll={handleScroll}\n ref={(el) => {\n tabPanelRefs.current[tab.contentId] = el;\n if (el && isActive && isContentScrollable) {\n checkScrollState(el);\n }\n }}\n {...props}\n >\n {tab.content}\n </div>\n );\n }\n\n if (!isActive) {\n return null;\n }\n\n return (\n <div\n className={cn(\n {\n 'h-full overflow-y-auto': isContentScrollable,\n },\n contentClassName\n )}\n key={tab.contentId}\n onScroll={handleScroll}\n ref={(el) => {\n tabPanelRefs.current[tab.contentId] = el;\n if (el && isContentScrollable) {\n checkScrollState(el);\n }\n }}\n {...props}\n >\n {tab.content}\n </div>\n );\n });\n // Handle controlled tabs - when currentTab prop changes externally, update internal state and scroll tab into view\n useEffect(() => {\n if (currentTab && currentTab !== selectedTab) {\n setSelectedTab(currentTab);\n scrollToTop(currentTab);\n\n // Scroll the tab button into view when controlled externally\n const tabButton = tabButtonRefs.current[currentTab];\n if (tabButton && typeof tabButton.scrollIntoView === 'function') {\n tabButton.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n });\n }\n }\n }, [currentTab, selectedTab, scrollToTop]);\n\n // Scroll the selected tab into view on initial render\n // Only scroll if it's not the first tab (first tab is already visible)\n // This is useful for bottom sheet on mobile where a non-first tab might be selected initially\n useEffect(() => {\n // Skip scroll if it's the first tab to prevent page jumping on initial load\n if (selectedTab === tabs[0].contentId) {\n return;\n }\n\n const tabButton = tabButtonRefs.current[selectedTab];\n if (tabButton && typeof tabButton.scrollIntoView === 'function') {\n tabButton.scrollIntoView({\n behavior: 'instant',\n block: 'nearest',\n inline: 'center',\n });\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Only run on mount\n\n // Check scroll state when active tab changes or content loads\n useEffect(() => {\n if (isContentScrollable) {\n const activePanel = tabPanelRefs.current[activeTab];\n if (activePanel) {\n // Match BottomSheet opening animation duration to ensure layout is stable\n const timer = setTimeout(() => {\n checkScrollState(activePanel);\n }, 300);\n\n return () => clearTimeout(timer);\n }\n }\n\n return undefined;\n }, [activeTab, isContentScrollable, checkScrollState, tabs]);\n\n return (\n <div\n className={cn(className, {\n 'flex h-full min-h-0 flex-1 flex-col': isContentScrollable,\n })}\n >\n <div\n className=\"relative flex-shrink-0 overflow-x-clip\"\n role=\"tablist\"\n >\n <div\n className=\"flex gap-sm overflow-x-auto xsMax:scrollbar-hide\"\n ref={horizontalScrollFadeProps.ref}\n >\n {/* Left fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute left-0 top-0 z-10 w-[48px] bg-gradient-to-r from-background-primary to-transparent transition-opacity duration-300 ease-in-out',\n {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n 'opacity-0': !horizontalScrollFadeProps.shouldShowLeftFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowLeftFade,\n }\n )}\n />\n\n {/* Right fade overlay */}\n <div\n className={cn(\n 'pointer-events-none absolute right-0 top-0 z-10 w-[48px] bg-gradient-to-l from-background-primary to-transparent transition-opacity duration-300 ease-in-out',\n {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n 'opacity-0': !horizontalScrollFadeProps.shouldShowRightFade,\n 'opacity-100': horizontalScrollFadeProps.shouldShowRightFade,\n }\n )}\n />\n\n {renderedTabs}\n <div\n className={cn('absolute left-0 right-0 z-[1] h-px border-b border-border-secondary', {\n 'bottom-0': !isContentScrollable,\n 'bottom-2xs': isContentScrollable,\n })}\n role=\"none\"\n />\n </div>\n </div>\n <div\n className={cn('relative', {\n 'min-h-0 flex-grow pb-3xs': isContentScrollable,\n })}\n >\n {renderedTabPanels}\n {/* Bottom gradient overlay - shows when content is scrollable and not at bottom */}\n {isContentScrollable && (\n <div\n className={cn(\n 'pointer-events-none absolute bottom-0 left-0 right-0 h-xl transition-opacity duration-300 ease-in-out',\n {\n 'opacity-0': isScrolledToBottom && isScrollable,\n 'opacity-100': !isScrolledToBottom && isScrollable,\n }\n )}\n data-testid=\"tabs-gradient-overlay\"\n style={{\n background: `linear-gradient(to bottom, transparent, ${wrapVarRgb('--color-background-primary')})`,\n }}\n />\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
package/tabs.mjs
CHANGED
|
@@ -38,7 +38,7 @@ const Tabs = ({ className, contentClassName, currentTab, defaultTab, isContentSc
|
|
|
38
38
|
}, [isContentScrollable, checkScrollState]);
|
|
39
39
|
const renderedTabs = tabs.map((tab) => {
|
|
40
40
|
return /* @__PURE__ */ jsx("button", {
|
|
41
|
-
className: cn("hui-text-h5", "z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none", "hui-focus-visible-outline focus
|
|
41
|
+
className: cn("hui-text-h5", "z-[2] overflow-visible whitespace-nowrap border-b-4 border-b-transparent bg-transparent p-0 outline-none", "hui-focus-visible-outline focus-visible:-outline-offset-1", "cursor-pointer text-text-secondary", { "hover:border-b-border-hover hover:text-link-hover": tab.contentId !== activeTab }, { "mb-2xs": isContentScrollable }, { "active:border-b-border-selected active:text-text-primary": tab.contentId !== activeTab }, { "border-b-border-selected text-text-primary": tab.contentId === activeTab }),
|
|
42
42
|
onClick: (e) => {
|
|
43
43
|
if (typeof e.currentTarget.scrollIntoView === "function") e.currentTarget.scrollIntoView({
|
|
44
44
|
behavior: "smooth",
|