@udixio/ui-react 2.10.13 → 2.10.15

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.
Files changed (173) hide show
  1. package/package.json +4 -2
  2. package/.eslintrc.mjs +0 -22
  3. package/.storybook/main.ts +0 -20
  4. package/.storybook/preview.ts +0 -1
  5. package/CHANGELOG.md +0 -1144
  6. package/postcss.config.mjs +0 -5
  7. package/src/index.css +0 -4
  8. package/src/index.ts +0 -1
  9. package/src/lib/components/AnchorPositioner.tsx +0 -185
  10. package/src/lib/components/Button.tsx +0 -208
  11. package/src/lib/components/Card.tsx +0 -47
  12. package/src/lib/components/Carousel.tsx +0 -437
  13. package/src/lib/components/CarouselItem.tsx +0 -61
  14. package/src/lib/components/Checkbox.tsx +0 -120
  15. package/src/lib/components/Chip.tsx +0 -341
  16. package/src/lib/components/Chips.tsx +0 -331
  17. package/src/lib/components/ContextMenu.tsx +0 -109
  18. package/src/lib/components/DatePicker.tsx +0 -432
  19. package/src/lib/components/Divider.tsx +0 -20
  20. package/src/lib/components/Fab.tsx +0 -127
  21. package/src/lib/components/FabMenu.tsx +0 -239
  22. package/src/lib/components/IconButton.tsx +0 -146
  23. package/src/lib/components/Menu.tsx +0 -88
  24. package/src/lib/components/MenuGroup.tsx +0 -34
  25. package/src/lib/components/MenuHeadline.tsx +0 -9
  26. package/src/lib/components/MenuItem.tsx +0 -215
  27. package/src/lib/components/NavigationRail.tsx +0 -186
  28. package/src/lib/components/NavigationRailItem.tsx +0 -227
  29. package/src/lib/components/ProgressIndicator.tsx +0 -214
  30. package/src/lib/components/SideSheet.tsx +0 -135
  31. package/src/lib/components/Slider.tsx +0 -374
  32. package/src/lib/components/Snackbar.tsx +0 -77
  33. package/src/lib/components/Switch.tsx +0 -107
  34. package/src/lib/components/Tab.tsx +0 -123
  35. package/src/lib/components/TabGroup.tsx +0 -66
  36. package/src/lib/components/TabGroupContext.tsx +0 -16
  37. package/src/lib/components/TabPanel.tsx +0 -27
  38. package/src/lib/components/TabPanels.tsx +0 -76
  39. package/src/lib/components/Tabs.tsx +0 -105
  40. package/src/lib/components/TextField.tsx +0 -586
  41. package/src/lib/components/Tooltip.tsx +0 -217
  42. package/src/lib/components/index.ts +0 -34
  43. package/src/lib/config/config.interface.ts +0 -9
  44. package/src/lib/config/define-config.ts +0 -16
  45. package/src/lib/config/index.ts +0 -2
  46. package/src/lib/effects/AnimateOnScroll.ts +0 -391
  47. package/src/lib/effects/State.tsx +0 -90
  48. package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
  49. package/src/lib/effects/ThemeProvider.tsx +0 -174
  50. package/src/lib/effects/block-scroll.effect.tsx +0 -313
  51. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
  52. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
  53. package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
  54. package/src/lib/effects/custom-scroll/index.ts +0 -3
  55. package/src/lib/effects/index.ts +0 -7
  56. package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
  57. package/src/lib/effects/ripple/index.tsx +0 -1
  58. package/src/lib/effects/scrollDriven.ts +0 -239
  59. package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
  60. package/src/lib/effects/theme.worker.ts +0 -97
  61. package/src/lib/hooks/index.ts +0 -10
  62. package/src/lib/hooks/useTooltipTrigger.ts +0 -270
  63. package/src/lib/icon/icon.tsx +0 -125
  64. package/src/lib/icon/index.ts +0 -1
  65. package/src/lib/index.ts +0 -8
  66. package/src/lib/interfaces/button.interface.ts +0 -65
  67. package/src/lib/interfaces/card.interface.ts +0 -11
  68. package/src/lib/interfaces/carousel-item.interface.ts +0 -12
  69. package/src/lib/interfaces/carousel.interface.ts +0 -41
  70. package/src/lib/interfaces/checkbox.interface.ts +0 -39
  71. package/src/lib/interfaces/chip.interface.ts +0 -97
  72. package/src/lib/interfaces/chips.interface.ts +0 -37
  73. package/src/lib/interfaces/date-picker.interface.ts +0 -79
  74. package/src/lib/interfaces/divider.interface.ts +0 -7
  75. package/src/lib/interfaces/fab-menu.interface.ts +0 -12
  76. package/src/lib/interfaces/fab.interface.ts +0 -27
  77. package/src/lib/interfaces/icon-button.interface.ts +0 -38
  78. package/src/lib/interfaces/index.ts +0 -26
  79. package/src/lib/interfaces/menu-group.interface.ts +0 -13
  80. package/src/lib/interfaces/menu-item.interface.ts +0 -29
  81. package/src/lib/interfaces/menu.interface.ts +0 -19
  82. package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
  83. package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
  84. package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
  85. package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
  86. package/src/lib/interfaces/slider.interface.ts +0 -27
  87. package/src/lib/interfaces/snackbar.interface.ts +0 -13
  88. package/src/lib/interfaces/switch.interface.ts +0 -14
  89. package/src/lib/interfaces/tab-group.interface.ts +0 -13
  90. package/src/lib/interfaces/tab-panels.interface.ts +0 -21
  91. package/src/lib/interfaces/tab.interface.ts +0 -31
  92. package/src/lib/interfaces/tabs.interface.ts +0 -22
  93. package/src/lib/interfaces/text-field.interface.ts +0 -61
  94. package/src/lib/interfaces/tooltip.interface.ts +0 -61
  95. package/src/lib/styles/button.style.ts +0 -136
  96. package/src/lib/styles/card.style.ts +0 -29
  97. package/src/lib/styles/carousel-item.style.ts +0 -24
  98. package/src/lib/styles/carousel.style.ts +0 -22
  99. package/src/lib/styles/checkbox.style.ts +0 -64
  100. package/src/lib/styles/chip.style.ts +0 -62
  101. package/src/lib/styles/chips.style.ts +0 -20
  102. package/src/lib/styles/date-picker.style.ts +0 -43
  103. package/src/lib/styles/divider.style.ts +0 -31
  104. package/src/lib/styles/fab-menu.style.ts +0 -29
  105. package/src/lib/styles/fab.style.ts +0 -49
  106. package/src/lib/styles/icon-button.style.ts +0 -168
  107. package/src/lib/styles/index.ts +0 -25
  108. package/src/lib/styles/menu-group.style.ts +0 -34
  109. package/src/lib/styles/menu-headline.style.ts +0 -20
  110. package/src/lib/styles/menu-item.style.ts +0 -45
  111. package/src/lib/styles/menu.style.ts +0 -32
  112. package/src/lib/styles/navigation-rail-item.style.ts +0 -56
  113. package/src/lib/styles/navigation-rail.style.ts +0 -36
  114. package/src/lib/styles/progress-indicator.style.ts +0 -72
  115. package/src/lib/styles/side-sheet.style.ts +0 -45
  116. package/src/lib/styles/slider.style.ts +0 -41
  117. package/src/lib/styles/snackbar.style.ts +0 -26
  118. package/src/lib/styles/switch.style.ts +0 -67
  119. package/src/lib/styles/tab-panels.style.ts +0 -35
  120. package/src/lib/styles/tab.style.ts +0 -78
  121. package/src/lib/styles/tabs.style.ts +0 -22
  122. package/src/lib/styles/text-field.style.ts +0 -115
  123. package/src/lib/styles/tooltip.style.ts +0 -48
  124. package/src/lib/utils/component-helper.ts +0 -134
  125. package/src/lib/utils/component.ts +0 -34
  126. package/src/lib/utils/index.ts +0 -7
  127. package/src/lib/utils/string.ts +0 -9
  128. package/src/lib/utils/styles/classnames.ts +0 -49
  129. package/src/lib/utils/styles/get-classname.ts +0 -96
  130. package/src/lib/utils/styles/index.ts +0 -4
  131. package/src/lib/utils/styles/use-classnames.ts +0 -25
  132. package/src/stories/action/button.stories.tsx +0 -86
  133. package/src/stories/action/fab.stories.tsx +0 -54
  134. package/src/stories/action/icon-button.stories.tsx +0 -134
  135. package/src/stories/assets/accessibility.png +0 -0
  136. package/src/stories/assets/accessibility.svg +0 -5
  137. package/src/stories/assets/addon-library.png +0 -0
  138. package/src/stories/assets/assets.png +0 -0
  139. package/src/stories/assets/context.png +0 -0
  140. package/src/stories/assets/discord.svg +0 -15
  141. package/src/stories/assets/docs.png +0 -0
  142. package/src/stories/assets/figma-plugin.png +0 -0
  143. package/src/stories/assets/github.svg +0 -3
  144. package/src/stories/assets/share.png +0 -0
  145. package/src/stories/assets/styling.png +0 -0
  146. package/src/stories/assets/testing.png +0 -0
  147. package/src/stories/assets/theming.png +0 -0
  148. package/src/stories/assets/tutorials.svg +0 -12
  149. package/src/stories/assets/youtube.svg +0 -4
  150. package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
  151. package/src/stories/communication/SnackBar.stories.tsx +0 -32
  152. package/src/stories/communication/tool-tip.stories.tsx +0 -133
  153. package/src/stories/containment/card.stories.tsx +0 -42
  154. package/src/stories/containment/carousel.stories.tsx +0 -65
  155. package/src/stories/containment/divider.stories.tsx +0 -35
  156. package/src/stories/containment/slide-sheet.stories.tsx +0 -45
  157. package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
  158. package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
  159. package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
  160. package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
  161. package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
  162. package/src/stories/selection/slider.stories.tsx +0 -85
  163. package/src/stories/selection/switch.stories.tsx +0 -46
  164. package/src/stories/text-inputs/text-field.stories.tsx +0 -135
  165. package/src/tests/Button.spec.tsx +0 -67
  166. package/src/tests/useClassNames.spec.tsx +0 -82
  167. package/src/udixio.css +0 -120
  168. package/theme.config.ts +0 -7
  169. package/tsconfig.json +0 -16
  170. package/tsconfig.lib.json +0 -51
  171. package/tsconfig.spec.json +0 -37
  172. package/tsconfig.storybook.json +0 -38
  173. package/vite.config.ts +0 -96
@@ -1,90 +0,0 @@
1
- import { RippleEffect } from './ripple';
2
- import {
3
- ClassNameComponent,
4
- classNames,
5
- createUseClassNames,
6
- ReactProps,
7
- } from '../utils';
8
- import { useEffect, useRef, useState } from 'react';
9
-
10
- export interface StateInterface {
11
- type: 'div';
12
- props: {
13
- colorName: string;
14
- stateClassName?:
15
- | string
16
- | 'state-ripple-group'
17
- | 'state-group'
18
- | 'state-layer';
19
- className?: string;
20
- style?: React.CSSProperties;
21
- children?: React.ReactNode;
22
- };
23
- states: { isClient: boolean };
24
- elements: ['stateLayer'];
25
- }
26
-
27
- export const State = ({
28
- style,
29
- colorName,
30
- stateClassName = 'state-ripple-group',
31
- children,
32
- className,
33
- }: ReactProps<StateInterface>) => {
34
- const ref = useRef<HTMLDivElement>(null);
35
- const groupStateRef = useRef<HTMLElement | null>(null);
36
-
37
- const [isClient, setIsClient] = useState(false);
38
- const styles = useStateStyle({
39
- isClient,
40
- stateClassName,
41
- className,
42
- colorName,
43
- });
44
-
45
- useEffect(() => {
46
- if (ref.current && stateClassName !== 'state-layer') {
47
- const groupName = !stateClassName.includes('[')
48
- ? 'group'
49
- : 'group/' + stateClassName.split('[')[1].split(']')[0];
50
-
51
- // On échappe le slash pour le sélecteur CSS
52
- const safeGroupName = groupName.replace(/\//g, '\\/');
53
-
54
- const furthestGroupState = ref.current.closest(
55
- `.${safeGroupName}:not(.${safeGroupName} .${safeGroupName})`,
56
- );
57
- groupStateRef.current = furthestGroupState as HTMLElement | null;
58
- }
59
- setIsClient(true);
60
- }, []);
61
-
62
- return (
63
- <div
64
- ref={ref}
65
- className={styles.stateLayer}
66
- style={{
67
- ['--state-color' as any]: `var(--default-color, var(--color-${colorName}))`,
68
- ...style,
69
- }}
70
- >
71
- {isClient && <RippleEffect triggerRef={groupStateRef} />}
72
- {children}
73
- </div>
74
- );
75
- };
76
- // ... existing code ...
77
- const cardConfig: ClassNameComponent<StateInterface> = ({
78
- isClient,
79
- stateClassName,
80
- }) => ({
81
- stateLayer: classNames([
82
- 'w-full top-0 left-0 h-full absolute pointer-events-none overflow-hidden',
83
- stateClassName,
84
- ]),
85
- });
86
-
87
- export const useStateStyle = createUseClassNames<StateInterface>(
88
- 'stateLayer',
89
- cardConfig,
90
- );
@@ -1,62 +0,0 @@
1
- import {
2
- type ReactNode,
3
- type RefObject,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from 'react';
8
- import { createPortal } from 'react-dom';
9
-
10
- type SyncedFixedWrapperProps = {
11
- targetRef: RefObject<any>;
12
- children: ReactNode;
13
- };
14
-
15
- export const SyncedFixedWrapper = ({
16
- targetRef,
17
- children,
18
- }: SyncedFixedWrapperProps) => {
19
- const [style, setStyle] = useState<React.CSSProperties | null>(null);
20
- const resizeObserver = useRef<ResizeObserver | null>(null);
21
-
22
- const updatePosition = () => {
23
- const target = targetRef.current;
24
- if (!target) return;
25
-
26
- const rect = target.getBoundingClientRect();
27
-
28
- setStyle({
29
- position: 'fixed',
30
- top: rect.top,
31
- left: rect.left,
32
- width: rect.width,
33
- height: rect.height,
34
- pointerEvents: 'none', // si le wrapper ne doit pas capter les events
35
- zIndex: 999, // personnalise si besoin
36
- });
37
- };
38
-
39
- useEffect(() => {
40
- const target = targetRef.current;
41
- if (!target) return;
42
-
43
- updatePosition();
44
-
45
- window.addEventListener('scroll', updatePosition, true);
46
- window.addEventListener('resize', updatePosition);
47
-
48
- // Observe resize of the target element
49
- resizeObserver.current = new ResizeObserver(updatePosition);
50
- resizeObserver.current.observe(target);
51
-
52
- return () => {
53
- window.removeEventListener('scroll', updatePosition, true);
54
- window.removeEventListener('resize', updatePosition);
55
- resizeObserver.current?.disconnect();
56
- };
57
- }, [targetRef]);
58
-
59
- if (!style) return null;
60
-
61
- return createPortal(<div style={style}>{children}</div>, document.body);
62
- };
@@ -1,174 +0,0 @@
1
- import {
2
- type API,
3
- type ConfigInterface,
4
- ContextOptions,
5
- FontPlugin,
6
- loader,
7
- serializeThemeContext,
8
- } from '@udixio/theme';
9
- import { useEffect, useRef, useState } from 'react';
10
- import { TailwindPlugin } from '@udixio/tailwind';
11
- import type {
12
- WorkerInboundMessage,
13
- WorkerOutboundMessage,
14
- } from './theme.worker';
15
-
16
- function isValidHexColor(hexColorString: string) {
17
- const regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
18
- return regex.test(hexColorString);
19
- }
20
-
21
- export const ThemeProvider = ({
22
- config,
23
- throttleDelay = 100,
24
- onLoad,
25
- loadTheme = false,
26
- }: {
27
- config: Readonly<ConfigInterface>;
28
- onLoad?: (api: API) => void;
29
- throttleDelay?: number;
30
- loadTheme?: boolean;
31
- }) => {
32
- const [themeApi, setThemeApi] = useState<API | null>(null);
33
- const [outputCss, setOutputCss] = useState<string | null>(null);
34
-
35
- const workerRef = useRef<Worker | null>(null);
36
- const generationRef = useRef(0);
37
- const lastAppliedIdRef = useRef(0);
38
- const themeApiRef = useRef<API | null>(null);
39
- const firstLoadDoneRef = useRef(false);
40
- const onLoadRef = useRef(onLoad);
41
- useEffect(() => {
42
- onLoadRef.current = onLoad;
43
- }, [onLoad]);
44
-
45
- // Initialisation de l'API et du Worker
46
- useEffect(() => {
47
- let cancelled = false;
48
-
49
- (async () => {
50
- const api = await loader(config, loadTheme);
51
- if (cancelled) return;
52
-
53
- themeApiRef.current = api;
54
- setThemeApi(api);
55
-
56
- const worker = new Worker(
57
- new URL('./theme.worker.ts', import.meta.url),
58
- { type: 'module' },
59
- );
60
- workerRef.current = worker;
61
-
62
- worker.onmessage = (e: MessageEvent<WorkerOutboundMessage>) => {
63
- if (e.data.id > lastAppliedIdRef.current) {
64
- lastAppliedIdRef.current = e.data.id;
65
- firstLoadDoneRef.current = true;
66
- setOutputCss(e.data.css);
67
- onLoadRef.current?.(themeApiRef.current!);
68
- }
69
- };
70
- })();
71
-
72
- return () => {
73
- cancelled = true;
74
- workerRef.current?.terminate();
75
- workerRef.current = null;
76
- };
77
- }, []);
78
-
79
- // Throttle avec exécution en tête (leading) et en fin (trailing)
80
- const timeoutRef = useRef<NodeJS.Timeout | null>(null);
81
- const lastExecTimeRef = useRef<number>(0);
82
- const lastArgsRef = useRef<Partial<ContextOptions> | null>(null);
83
-
84
- useEffect(() => {
85
- if (!themeApi) return;
86
-
87
- const ctx: Partial<ContextOptions> = {
88
- ...config,
89
- sourceColor: config.sourceColor,
90
- };
91
-
92
- const now = Date.now();
93
- const timeSinceLast = now - lastExecTimeRef.current;
94
-
95
- const invoke = async (args: Partial<ContextOptions>) => {
96
- await applyThemeChange(args);
97
- };
98
-
99
- if (lastExecTimeRef.current === 0 || timeSinceLast >= throttleDelay) {
100
- if (timeoutRef.current) {
101
- clearTimeout(timeoutRef.current);
102
- timeoutRef.current = null;
103
- }
104
- lastArgsRef.current = null;
105
- lastExecTimeRef.current = now;
106
- void invoke(ctx);
107
- } else {
108
- lastArgsRef.current = ctx;
109
- if (!timeoutRef.current) {
110
- const remaining = Math.max(0, throttleDelay - timeSinceLast);
111
- timeoutRef.current = setTimeout(async () => {
112
- timeoutRef.current = null;
113
- const args = lastArgsRef.current;
114
- lastArgsRef.current = null;
115
- if (args) {
116
- lastExecTimeRef.current = Date.now();
117
- await invoke(args);
118
- }
119
- }, remaining);
120
- }
121
- }
122
-
123
- return () => {};
124
- }, [config, throttleDelay, themeApi]);
125
-
126
- const applyThemeChange = async (ctx: Partial<ContextOptions>) => {
127
- if (typeof ctx.sourceColor === 'string' && !isValidHexColor(ctx.sourceColor)) {
128
- throw new Error('Invalid hex color');
129
- }
130
-
131
- const api = themeApiRef.current;
132
- if (!api) return;
133
-
134
- // Toujours évaluer sur le main thread (rapide)
135
- api.context.update(ctx);
136
- api.palettes.sync((ctx as any).palettes);
137
-
138
- const worker = workerRef.current;
139
-
140
- // Fallback synchrone : premier rendu ou Worker pas encore prêt
141
- if (!worker || !firstLoadDoneRef.current) {
142
- await api.load();
143
- const css = api.plugins.getPlugin(TailwindPlugin).getInstance().outputCss;
144
- setOutputCss(css);
145
- firstLoadDoneRef.current = true;
146
- onLoad?.(api);
147
- return;
148
- }
149
-
150
- // Offload au Worker
151
- const id = ++generationRef.current;
152
- worker.postMessage({
153
- id,
154
- snapshot: serializeThemeContext(api),
155
- tailwindOptions: api.plugins.getPlugin(TailwindPlugin).options,
156
- fontOptions: api.plugins.getPlugin(FontPlugin).options,
157
- } satisfies WorkerInboundMessage);
158
- };
159
-
160
- useEffect(() => {
161
- return () => {
162
- if (timeoutRef.current) {
163
- clearTimeout(timeoutRef.current);
164
- timeoutRef.current = null;
165
- }
166
- };
167
- }, []);
168
-
169
- if (!outputCss) {
170
- return null;
171
- }
172
-
173
- return <style dangerouslySetInnerHTML={{ __html: outputCss }} />;
174
- };
@@ -1,313 +0,0 @@
1
- import React, { useEffect, useRef } from 'react';
2
-
3
- type ScrollIntent =
4
- | {
5
- type: 'intent';
6
- source: 'wheel' | 'touch' | 'keyboard';
7
- deltaX: number;
8
- deltaY: number;
9
- originalEvent: Event;
10
- }
11
- | {
12
- type: 'scrollbar';
13
- scrollTop: number;
14
- scrollLeft: number;
15
- maxScrollTop: number;
16
- maxScrollLeft: number;
17
- };
18
-
19
- type BlockScrollProps = {
20
- onScroll?: (evt: ScrollIntent) => void; // log des intentions + du scroll via scrollbar
21
- touch?: boolean;
22
- el: HTMLElement;
23
- };
24
-
25
- /**
26
- * @deprecated Potentially blocks scroll events unintentionally (wheel/touch/keyboard)
27
- * and may interfere with internal scrollable areas (modals, lists, overflow containers).
28
- * This API will be removed soon. Avoid using it and migrate to native behaviors
29
- * or to local scroll handling at the component level.
30
- */
31
- export const BlockScroll: React.FC<BlockScrollProps> = ({
32
- onScroll,
33
- el,
34
- touch = true,
35
- }) => {
36
- const lastTouch = useRef<{ x: number; y: number } | null>(null);
37
- const lastScrollTop = useRef<number>(0);
38
- const lastScrollLeft = useRef<number>(0);
39
-
40
- useEffect(() => {
41
- if (!el) return;
42
-
43
- // Initialize last known scroll positions to block scrollbar-based scrolling
44
- lastScrollTop.current = el.scrollTop;
45
- lastScrollLeft.current = el.scrollLeft;
46
-
47
- const emitIntent = (payload: Extract<ScrollIntent, { type: 'intent' }>) => {
48
- // Log the desired deltaY for every scroll attempt (wheel/touch/keyboard)
49
- onScroll?.(payload);
50
- };
51
-
52
- const findScrollableParent = (
53
- node: HTMLElement | null,
54
- ): HTMLElement | null => {
55
- let elNode: HTMLElement | null = node;
56
- while (
57
- elNode &&
58
- elNode !== document.body &&
59
- elNode !== document.documentElement
60
- ) {
61
- const style = window.getComputedStyle(elNode);
62
- const overflowY = style.overflowY || style.overflow;
63
- const isScrollableY =
64
- (overflowY === 'auto' || overflowY === 'scroll') &&
65
- elNode.scrollHeight > elNode.clientHeight;
66
- if (isScrollableY) return elNode;
67
- elNode = elNode.parentElement;
68
- }
69
- return null;
70
- };
71
-
72
- const onWheel = (e: WheelEvent) => {
73
- // Auto-detect closest scrollable ancestor and allow native scroll when it can handle the intent
74
- const target = e.target as HTMLElement | null;
75
-
76
- const scrollableParent = findScrollableParent(target);
77
-
78
- if (scrollableParent && scrollableParent !== el) {
79
- const canScrollDown =
80
- scrollableParent.scrollTop <
81
- scrollableParent.scrollHeight - scrollableParent.clientHeight;
82
- const canScrollUp = scrollableParent.scrollTop > 0;
83
-
84
- // Wheel: positive deltaY => scroll down, negative => scroll up
85
- if ((e.deltaY > 0 && canScrollDown) || (e.deltaY < 0 && canScrollUp)) {
86
- // Let the native scrolling happen inside the scrollable element
87
- return;
88
- }
89
- }
90
-
91
- // Otherwise, block native scroll and emit intent for global smooth scroll
92
- e.preventDefault();
93
- emitIntent({
94
- type: 'intent',
95
- source: 'wheel',
96
- deltaX: e.deltaX,
97
- deltaY: e.deltaY,
98
- originalEvent: e,
99
- });
100
- };
101
-
102
- const onTouchStart = (e: TouchEvent) => {
103
- if (!touch) return;
104
- const t = e.touches[0];
105
- if (t) lastTouch.current = { x: t.clientX, y: t.clientY };
106
- };
107
-
108
- const onTouchMove = (e: TouchEvent) => {
109
- if (!touch) return;
110
- const t = e.touches[0];
111
- if (!t || !lastTouch.current) return;
112
-
113
- const dx = lastTouch.current.x - t.clientX;
114
- const dy = lastTouch.current.y - t.clientY;
115
-
116
- // Auto-detect closest scrollable ancestor for touch and allow native scroll when possible
117
- const target = e.target as HTMLElement | null;
118
-
119
- const scrollableParent = findScrollableParent(target);
120
-
121
- if (scrollableParent && scrollableParent !== el) {
122
- const canScrollDown =
123
- scrollableParent.scrollTop <
124
- scrollableParent.scrollHeight - scrollableParent.clientHeight;
125
- const canScrollUp = scrollableParent.scrollTop > 0;
126
-
127
- // Touch: dy > 0 means user moves finger up -> intent to scroll down
128
- if ((dy > 0 && canScrollDown) || (dy < 0 && canScrollUp)) {
129
- // Update last touch and allow native scroll inside the element
130
- lastTouch.current = { x: t.clientX, y: t.clientY };
131
- return;
132
- }
133
- }
134
-
135
- // Otherwise block and emit intent for global smooth scroll
136
- e.preventDefault();
137
- lastTouch.current = { x: t.clientX, y: t.clientY };
138
- emitIntent({
139
- type: 'intent',
140
- source: 'touch',
141
- deltaX: dx,
142
- deltaY: dy,
143
- originalEvent: e,
144
- });
145
- };
146
-
147
- const onTouchEnd = () => {
148
- if (!touch) return;
149
- lastTouch.current = null;
150
- };
151
-
152
- const interactiveRoles = new Set([
153
- 'textbox',
154
- 'listbox',
155
- 'menu',
156
- 'menubar',
157
- 'grid',
158
- 'tree',
159
- 'tablist',
160
- 'toolbar',
161
- 'radiogroup',
162
- 'combobox',
163
- 'spinbutton',
164
- 'slider',
165
- ]);
166
-
167
- const isEditableOrInteractive = (node: HTMLElement | null) => {
168
- if (!node) return false;
169
- const tag = node.tagName.toLowerCase();
170
- if (tag === 'input' || tag === 'textarea' || tag === 'select')
171
- return true;
172
- if ((node as HTMLInputElement).isContentEditable) return true;
173
- if (tag === 'button') return true;
174
- if (tag === 'a' && (node as HTMLAnchorElement).href) return true;
175
- const role = node.getAttribute('role');
176
- if (role && interactiveRoles.has(role)) return true;
177
- return false;
178
- };
179
-
180
- const hasInteractiveAncestor = (el: HTMLElement | null) => {
181
- let n: HTMLElement | null = el;
182
- while (n && n !== document.body && n !== document.documentElement) {
183
- if (isEditableOrInteractive(n)) return true;
184
- n = n.parentElement;
185
- }
186
- return false;
187
- };
188
-
189
- const canAncestorScroll = (start: HTMLElement | null, dy: number) => {
190
- let n: HTMLElement | null = start;
191
- while (n && n !== el) {
192
- const style = window.getComputedStyle(n);
193
- const overflowY = style.overflowY || style.overflow;
194
- const canScrollY =
195
- (overflowY === 'auto' || overflowY === 'scroll') &&
196
- n.scrollHeight > n.clientHeight;
197
- if (canScrollY) {
198
- const canDown = n.scrollTop < n.scrollHeight - n.clientHeight;
199
- const canUp = n.scrollTop > 0;
200
- if ((dy > 0 && canDown) || (dy < 0 && canUp)) return true;
201
- }
202
- n = n.parentElement;
203
- }
204
- return false;
205
- };
206
-
207
- const onKeyDown = (e: KeyboardEvent) => {
208
- if (e.defaultPrevented) return;
209
-
210
- // Garder les comportements natifs pour les éléments interactifs
211
- const target = e.target as HTMLElement | null;
212
- if (isEditableOrInteractive(target) || hasInteractiveAncestor(target)) {
213
- return; // ne pas empêcher
214
- }
215
-
216
- const line = 40;
217
- const page = el.clientHeight * 0.9;
218
- let dy = 0;
219
-
220
- switch (e.key) {
221
- case 'ArrowDown':
222
- dy = line;
223
- break;
224
- case 'ArrowUp':
225
- dy = -line;
226
- break;
227
- case 'PageDown':
228
- dy = page;
229
- break;
230
- case 'PageUp':
231
- dy = -page;
232
- break;
233
- case 'Home':
234
- dy = Number.NEGATIVE_INFINITY;
235
- break;
236
- case 'End':
237
- dy = Number.POSITIVE_INFINITY;
238
- break;
239
- case ' ': {
240
- // Espace: laisser passer sur boutons/inputs/etc. déjà filtrés ci-dessus
241
- dy = e.shiftKey ? -page : page;
242
- break;
243
- }
244
- default:
245
- return; // ne pas gérer, laisser natif
246
- }
247
-
248
- // Si un ancêtre (≠ el) peut scroller dans ce sens, laisser le natif
249
- if (canAncestorScroll(target, dy)) return;
250
-
251
- // Ne gérer que si focus est sur body/html ou dans el
252
- const ae = document.activeElement as HTMLElement | null;
253
- const focusInsideEl =
254
- ae &&
255
- (ae === document.body ||
256
- ae === document.documentElement ||
257
- el.contains(ae));
258
- if (!focusInsideEl) return;
259
-
260
- // OK on prend en charge: empêcher le natif et émettre l’intention
261
- e.preventDefault();
262
- emitIntent({
263
- type: 'intent',
264
- source: 'keyboard',
265
- deltaX: 0,
266
- deltaY: dy,
267
- originalEvent: e,
268
- });
269
- };
270
-
271
- // const onScrollEvent = (e) => {
272
- // const currentScrollTop = e.target.scrollTop;
273
- // const currentScrollLeft = e.target.scrollLeft;
274
- //
275
- // // Check if scroll position changed from last known position
276
- // if (
277
- // currentScrollTop !== lastScrollTop.current ||
278
- // currentScrollLeft !== lastScrollLeft.current
279
- // ) {
280
- // console.log('onScrollllllllllll', e, document);
281
- // onScroll?.({
282
- // type: 'scrollbar',
283
- // scrollTop: currentScrollTop,
284
- // scrollLeft: currentScrollLeft,
285
- // maxScrollTop: e.target.scrollHeight - e.target.clientHeight,
286
- // maxScrollLeft: e.target.scrollWidth - e.target.clientWidth,
287
- // });
288
- // }
289
- //
290
- // // Update last known scroll positions
291
- // lastScrollTop.current = currentScrollTop;
292
- // lastScrollLeft.current = currentScrollLeft;
293
- //
294
- // document.querySelector('html')?.scrollTo({ top: 0 });
295
- // };
296
-
297
- el.addEventListener('wheel', onWheel, { passive: false });
298
- el.addEventListener('touchstart', onTouchStart, { passive: true });
299
- el.addEventListener('touchmove', onTouchMove, { passive: false });
300
- el.addEventListener('touchend', onTouchEnd, { passive: true });
301
- el.addEventListener('keydown', onKeyDown);
302
- // el.addEventListener('scroll', onScrollEvent, { passive: true });
303
-
304
- return () => {
305
- el.removeEventListener('wheel', onWheel as EventListener);
306
- el.removeEventListener('touchstart', onTouchStart as EventListener);
307
- el.removeEventListener('touchmove', onTouchMove as EventListener);
308
- el.removeEventListener('touchend', onTouchEnd as EventListener);
309
- el.removeEventListener('keydown', onKeyDown as EventListener);
310
- // el.removeEventListener('scroll', onScrollEvent as EventListener);
311
- };
312
- }, [onScroll]);
313
- };