@octanejs/aria 0.0.3 → 0.0.5

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 (93) hide show
  1. package/package.json +7 -5
  2. package/src/autocomplete/useAutocomplete.ts +656 -0
  3. package/src/collections/BaseCollection.ts +374 -0
  4. package/src/collections/CollectionBuilder.ts +343 -0
  5. package/src/collections/Document.ts +451 -0
  6. package/src/collections/Hidden.ts +84 -0
  7. package/src/collections/useCachedChildren.ts +103 -0
  8. package/src/components/Autocomplete.ts +124 -0
  9. package/src/components/Breadcrumbs.ts +170 -0
  10. package/src/components/Button.ts +209 -0
  11. package/src/components/Checkbox.ts +593 -0
  12. package/src/components/Collection.ts +301 -0
  13. package/src/components/ComboBox.ts +477 -0
  14. package/src/components/Dialog.ts +195 -0
  15. package/src/components/Disclosure.ts +322 -0
  16. package/src/components/DragAndDrop.ts +177 -0
  17. package/src/components/FieldError.ts +79 -0
  18. package/src/components/Form.ts +65 -0
  19. package/src/components/GridList.ts +1099 -0
  20. package/src/components/Group.ts +146 -0
  21. package/src/components/Header.ts +32 -0
  22. package/src/components/Heading.ts +45 -0
  23. package/src/components/Input.ts +139 -0
  24. package/src/components/Keyboard.ts +23 -0
  25. package/src/components/Label.ts +30 -0
  26. package/src/components/Link.ts +137 -0
  27. package/src/components/ListBox.ts +886 -0
  28. package/src/components/Menu.ts +685 -0
  29. package/src/components/Meter.ts +100 -0
  30. package/src/components/Modal.ts +352 -0
  31. package/src/components/NumberField.ts +221 -0
  32. package/src/components/OverlayArrow.ts +117 -0
  33. package/src/components/Popover.ts +385 -0
  34. package/src/components/ProgressBar.ts +112 -0
  35. package/src/components/RadioGroup.ts +542 -0
  36. package/src/components/SearchField.ts +208 -0
  37. package/src/components/Select.ts +505 -0
  38. package/src/components/SelectionIndicator.ts +52 -0
  39. package/src/components/Separator.ts +97 -0
  40. package/src/components/SharedElementTransition.ts +209 -0
  41. package/src/components/Slider.ts +472 -0
  42. package/src/components/Switch.ts +449 -0
  43. package/src/components/Tabs.ts +675 -0
  44. package/src/components/TagGroup.ts +443 -0
  45. package/src/components/Text.ts +27 -0
  46. package/src/components/TextArea.ts +92 -0
  47. package/src/components/TextField.ts +200 -0
  48. package/src/components/ToggleButton.ts +147 -0
  49. package/src/components/ToggleButtonGroup.ts +106 -0
  50. package/src/components/Toolbar.ts +74 -0
  51. package/src/components/Tooltip.ts +249 -0
  52. package/src/components/index.ts +302 -0
  53. package/src/components/useDragAndDrop.ts +164 -0
  54. package/src/components/utils.ts +521 -0
  55. package/src/intl/autocomplete/ar-AE.json +3 -0
  56. package/src/intl/autocomplete/bg-BG.json +3 -0
  57. package/src/intl/autocomplete/cs-CZ.json +3 -0
  58. package/src/intl/autocomplete/da-DK.json +3 -0
  59. package/src/intl/autocomplete/de-DE.json +3 -0
  60. package/src/intl/autocomplete/el-GR.json +3 -0
  61. package/src/intl/autocomplete/en-US.json +3 -0
  62. package/src/intl/autocomplete/es-ES.json +3 -0
  63. package/src/intl/autocomplete/et-EE.json +3 -0
  64. package/src/intl/autocomplete/fi-FI.json +3 -0
  65. package/src/intl/autocomplete/fr-FR.json +3 -0
  66. package/src/intl/autocomplete/he-IL.json +3 -0
  67. package/src/intl/autocomplete/hr-HR.json +3 -0
  68. package/src/intl/autocomplete/hu-HU.json +3 -0
  69. package/src/intl/autocomplete/index.ts +75 -0
  70. package/src/intl/autocomplete/it-IT.json +3 -0
  71. package/src/intl/autocomplete/ja-JP.json +3 -0
  72. package/src/intl/autocomplete/ko-KR.json +3 -0
  73. package/src/intl/autocomplete/lt-LT.json +3 -0
  74. package/src/intl/autocomplete/lv-LV.json +3 -0
  75. package/src/intl/autocomplete/nb-NO.json +3 -0
  76. package/src/intl/autocomplete/nl-NL.json +3 -0
  77. package/src/intl/autocomplete/pl-PL.json +3 -0
  78. package/src/intl/autocomplete/pt-BR.json +3 -0
  79. package/src/intl/autocomplete/pt-PT.json +3 -0
  80. package/src/intl/autocomplete/ro-RO.json +3 -0
  81. package/src/intl/autocomplete/ru-RU.json +3 -0
  82. package/src/intl/autocomplete/sk-SK.json +3 -0
  83. package/src/intl/autocomplete/sl-SI.json +3 -0
  84. package/src/intl/autocomplete/sr-SP.json +3 -0
  85. package/src/intl/autocomplete/sv-SE.json +3 -0
  86. package/src/intl/autocomplete/tr-TR.json +3 -0
  87. package/src/intl/autocomplete/uk-UA.json +3 -0
  88. package/src/intl/autocomplete/zh-CN.json +3 -0
  89. package/src/intl/autocomplete/zh-TW.json +3 -0
  90. package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
  91. package/src/utils/animation.ts +149 -0
  92. package/src/utils/useLoadMoreSentinel.ts +78 -0
  93. package/src/utils/useViewportSize.ts +123 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Forslag"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Empfehlungen"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Προτάσεις"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Suggestions"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Sugerencias"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Soovitused"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Ehdotukset"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Suggestions"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "הצעות"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Prijedlozi"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Javaslatok"
3
+ }
@@ -0,0 +1,75 @@
1
+ // Generated locale-dictionary index for the autocomplete area — the octane equivalent of
2
+ // upstream's Parcel glob import (`import intlMessages from '../../intl/autocomplete/*.json'`).
3
+ // The JSON files are copied VERBATIM from .react-spectrum/packages/react-aria/intl/autocomplete/;
4
+ // refresh them from the pinned checkout on version bumps, never hand-edit.
5
+ import ar_AE from './ar-AE.json';
6
+ import bg_BG from './bg-BG.json';
7
+ import cs_CZ from './cs-CZ.json';
8
+ import da_DK from './da-DK.json';
9
+ import de_DE from './de-DE.json';
10
+ import el_GR from './el-GR.json';
11
+ import en_US from './en-US.json';
12
+ import es_ES from './es-ES.json';
13
+ import et_EE from './et-EE.json';
14
+ import fi_FI from './fi-FI.json';
15
+ import fr_FR from './fr-FR.json';
16
+ import he_IL from './he-IL.json';
17
+ import hr_HR from './hr-HR.json';
18
+ import hu_HU from './hu-HU.json';
19
+ import it_IT from './it-IT.json';
20
+ import ja_JP from './ja-JP.json';
21
+ import ko_KR from './ko-KR.json';
22
+ import lt_LT from './lt-LT.json';
23
+ import lv_LV from './lv-LV.json';
24
+ import nb_NO from './nb-NO.json';
25
+ import nl_NL from './nl-NL.json';
26
+ import pl_PL from './pl-PL.json';
27
+ import pt_BR from './pt-BR.json';
28
+ import pt_PT from './pt-PT.json';
29
+ import ro_RO from './ro-RO.json';
30
+ import ru_RU from './ru-RU.json';
31
+ import sk_SK from './sk-SK.json';
32
+ import sl_SI from './sl-SI.json';
33
+ import sr_SP from './sr-SP.json';
34
+ import sv_SE from './sv-SE.json';
35
+ import tr_TR from './tr-TR.json';
36
+ import uk_UA from './uk-UA.json';
37
+ import zh_CN from './zh-CN.json';
38
+ import zh_TW from './zh-TW.json';
39
+
40
+ export default {
41
+ 'ar-AE': ar_AE,
42
+ 'bg-BG': bg_BG,
43
+ 'cs-CZ': cs_CZ,
44
+ 'da-DK': da_DK,
45
+ 'de-DE': de_DE,
46
+ 'el-GR': el_GR,
47
+ 'en-US': en_US,
48
+ 'es-ES': es_ES,
49
+ 'et-EE': et_EE,
50
+ 'fi-FI': fi_FI,
51
+ 'fr-FR': fr_FR,
52
+ 'he-IL': he_IL,
53
+ 'hr-HR': hr_HR,
54
+ 'hu-HU': hu_HU,
55
+ 'it-IT': it_IT,
56
+ 'ja-JP': ja_JP,
57
+ 'ko-KR': ko_KR,
58
+ 'lt-LT': lt_LT,
59
+ 'lv-LV': lv_LV,
60
+ 'nb-NO': nb_NO,
61
+ 'nl-NL': nl_NL,
62
+ 'pl-PL': pl_PL,
63
+ 'pt-BR': pt_BR,
64
+ 'pt-PT': pt_PT,
65
+ 'ro-RO': ro_RO,
66
+ 'ru-RU': ru_RU,
67
+ 'sk-SK': sk_SK,
68
+ 'sl-SI': sl_SI,
69
+ 'sr-SP': sr_SP,
70
+ 'sv-SE': sv_SE,
71
+ 'tr-TR': tr_TR,
72
+ 'uk-UA': uk_UA,
73
+ 'zh-CN': zh_CN,
74
+ 'zh-TW': zh_TW,
75
+ } as Record<string, Record<string, string>>;
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Suggerimenti"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "候補"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "제안"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Pasiūlymai"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Ieteikumi"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Forslag"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Suggesties"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Sugestie"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Sugestões"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Sugestões"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Sugestii"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Предложения"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Návrhy"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Predlogi"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Predlozi"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Förslag"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Öneriler"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "Пропозиції"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "建议"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "collectionLabel": "建議"
3
+ }
@@ -0,0 +1,85 @@
1
+ // Ported from react-stately (source: .react-spectrum/packages/react-stately/src/autocomplete/useAutocompleteState.ts).
2
+ // octane adaptations: public-hook slot threading (splitSlot/subSlot) per the binding
3
+ // convention; `ReactNode` → a structural `any` alias (octane renderables); the public
4
+ // value-level `onInputChange(value)` callback is unchanged (the onInput rule applies
5
+ // only to DOM wiring, which lives in useTextField/useAutocomplete).
6
+ import { useState } from 'octane';
7
+
8
+ import { S, splitSlot, subSlot } from '../../internal';
9
+ import { useControlledState } from '../utils/useControlledState';
10
+
11
+ // octane adaptation: structural alias for React's ReactNode.
12
+ type ReactNode = any;
13
+
14
+ export interface AutocompleteState {
15
+ /** The current value of the autocomplete input. */
16
+ inputValue: string;
17
+ /** Sets the value of the autocomplete input. */
18
+ setInputValue(value: string): void;
19
+ /** The id of the current aria-activedescendant of the autocomplete input. */
20
+ focusedNodeId: string | null;
21
+ /** Sets the id of the current aria-activedescendant of the autocomplete input. */
22
+ setFocusedNodeId(value: string | null): void;
23
+ }
24
+
25
+ export interface AutocompleteProps {
26
+ /** The value of the autocomplete input (controlled). */
27
+ inputValue?: string;
28
+ /** The default value of the autocomplete input (uncontrolled). */
29
+ defaultInputValue?: string;
30
+ /** Handler that is called when the autocomplete input value changes. */
31
+ onInputChange?: (value: string) => void;
32
+ /**
33
+ * The children wrapped by the autocomplete. Consists of at least an input element and a
34
+ * collection element to filter.
35
+ */
36
+ children: ReactNode;
37
+ }
38
+
39
+ // Emulate our other stately hooks which accept all "base" props even if not used
40
+ export interface AutocompleteStateOptions extends Omit<AutocompleteProps, 'children'> {}
41
+
42
+ /**
43
+ * Provides state management for an autocomplete component.
44
+ */
45
+ export function useAutocompleteState(props: AutocompleteStateOptions): AutocompleteState;
46
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
47
+ export function useAutocompleteState(
48
+ props: AutocompleteStateOptions,
49
+ slot: symbol | undefined,
50
+ ): AutocompleteState;
51
+ export function useAutocompleteState(...args: any[]): AutocompleteState {
52
+ const [user, slotArg] = splitSlot(args);
53
+ const slot = slotArg ?? S('useAutocompleteState');
54
+ const props = user[0] as AutocompleteStateOptions;
55
+
56
+ let {
57
+ onInputChange: propsOnInputChange,
58
+ inputValue: propsInputValue,
59
+ defaultInputValue: propsDefaultInputValue = '',
60
+ } = props;
61
+
62
+ let onInputChange = (value: string) => {
63
+ if (propsOnInputChange) {
64
+ propsOnInputChange(value);
65
+ }
66
+ };
67
+
68
+ let [focusedNodeId, setFocusedNodeId] = useState<string | null>(
69
+ null,
70
+ subSlot(slot, 'focusedNodeId'),
71
+ );
72
+ let [inputValue, setInputValue] = useControlledState(
73
+ propsInputValue,
74
+ propsDefaultInputValue!,
75
+ onInputChange,
76
+ subSlot(slot, 'inputValue'),
77
+ );
78
+
79
+ return {
80
+ inputValue,
81
+ setInputValue,
82
+ focusedNodeId,
83
+ setFocusedNodeId,
84
+ };
85
+ }
@@ -0,0 +1,149 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/animation.ts).
2
+ // octane adaptations: `flushSync` comes from 'octane' (not react-dom); public hooks get the
3
+ // binding's slot threading (splitSlot/subSlot); explicit dep arrays are preserved verbatim.
4
+ // In environments without `Element#getAnimations` (jsdom), the animation completes
5
+ // immediately, exactly like upstream's JSDOM branch.
6
+ import { flushSync, useCallback, useState } from 'octane';
7
+
8
+ import { S, splitSlot, subSlot } from '../internal';
9
+ import { useLayoutEffect } from './useLayoutEffect';
10
+
11
+ type RefObject<T> = { current: T };
12
+
13
+ export function useEnterAnimation(ref: RefObject<HTMLElement | null>, isReady?: boolean): boolean;
14
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
15
+ export function useEnterAnimation(
16
+ ref: RefObject<HTMLElement | null>,
17
+ isReady: boolean | undefined,
18
+ slot: symbol | undefined,
19
+ ): boolean;
20
+ export function useEnterAnimation(...args: any[]): boolean {
21
+ const [user, slotArg] = splitSlot(args);
22
+ const slot = slotArg ?? S('useEnterAnimation');
23
+ const ref = user[0] as RefObject<HTMLElement | null>;
24
+ const isReady = (user[1] as boolean | undefined) ?? true;
25
+
26
+ let [isEntering, setEntering] = useState(true, subSlot(slot, 'entering'));
27
+ let isAnimationReady = isEntering && isReady;
28
+
29
+ // There are two cases for entry animations:
30
+ // 1. CSS @keyframes. The `animation` property is set during the isEntering state, and it is removed after the animation finishes.
31
+ // 2. CSS transitions. The initial styles are applied during the isEntering state, and removed immediately, causing the transition to occur.
32
+ //
33
+ // In the second case, cancel any transitions that were triggered prior to the isEntering = false state (when the transition is supposed to start).
34
+ // This can happen when isReady starts as false (e.g. popovers prior to placement calculation).
35
+ useLayoutEffect(
36
+ () => {
37
+ if (isAnimationReady && ref.current && 'getAnimations' in ref.current) {
38
+ for (let animation of ref.current.getAnimations()) {
39
+ if (animation instanceof CSSTransition) {
40
+ animation.cancel();
41
+ }
42
+ }
43
+ }
44
+ },
45
+ [ref, isAnimationReady],
46
+ subSlot(slot, 'cancelTransitions'),
47
+ );
48
+
49
+ useAnimation(
50
+ ref,
51
+ isAnimationReady,
52
+ useCallback(() => setEntering(false), [], subSlot(slot, 'onEnd')),
53
+ subSlot(slot, 'animation'),
54
+ );
55
+ return isAnimationReady;
56
+ }
57
+
58
+ export function useExitAnimation(ref: RefObject<HTMLElement | null>, isOpen: boolean): boolean;
59
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
60
+ export function useExitAnimation(
61
+ ref: RefObject<HTMLElement | null>,
62
+ isOpen: boolean,
63
+ slot: symbol | undefined,
64
+ ): boolean;
65
+ export function useExitAnimation(...args: any[]): boolean {
66
+ const [user, slotArg] = splitSlot(args);
67
+ const slot = slotArg ?? S('useExitAnimation');
68
+ const ref = user[0] as RefObject<HTMLElement | null>;
69
+ const isOpen = user[1] as boolean;
70
+
71
+ let [exitState, setExitState] = useState<'closed' | 'open' | 'exiting'>(
72
+ isOpen ? 'open' : 'closed',
73
+ subSlot(slot, 'exitState'),
74
+ );
75
+
76
+ switch (exitState) {
77
+ case 'open':
78
+ // If isOpen becomes false, set the state to exiting.
79
+ if (!isOpen) {
80
+ setExitState('exiting');
81
+ }
82
+ break;
83
+ case 'closed':
84
+ case 'exiting':
85
+ // If we are exiting and isOpen becomes true, the animation was interrupted.
86
+ // Reset the state to open.
87
+ if (isOpen) {
88
+ setExitState('open');
89
+ }
90
+ break;
91
+ }
92
+
93
+ let isExiting = exitState === 'exiting';
94
+ useAnimation(
95
+ ref,
96
+ isExiting,
97
+ useCallback(
98
+ () => {
99
+ // Set the state to closed, which will cause the element to be unmounted.
100
+ setExitState((state) => (state === 'exiting' ? 'closed' : state));
101
+ },
102
+ [],
103
+ subSlot(slot, 'onEnd'),
104
+ ),
105
+ subSlot(slot, 'animation'),
106
+ );
107
+
108
+ return isExiting;
109
+ }
110
+
111
+ function useAnimation(
112
+ ref: RefObject<HTMLElement | null>,
113
+ isActive: boolean,
114
+ onEnd: () => void,
115
+ slot: symbol | undefined,
116
+ ): void {
117
+ useLayoutEffect(
118
+ () => {
119
+ if (isActive && ref.current) {
120
+ if (!('getAnimations' in ref.current)) {
121
+ // JSDOM
122
+ onEnd();
123
+ return;
124
+ }
125
+
126
+ let animations = ref.current.getAnimations();
127
+ if (animations.length === 0) {
128
+ onEnd();
129
+ return;
130
+ }
131
+
132
+ let canceled = false;
133
+ Promise.allSettled(animations.map((a) => a.finished)).then(() => {
134
+ if (!canceled) {
135
+ flushSync(() => {
136
+ onEnd();
137
+ });
138
+ }
139
+ });
140
+
141
+ return () => {
142
+ canceled = true;
143
+ };
144
+ }
145
+ },
146
+ [ref, isActive, onEnd],
147
+ subSlot(slot, 'effect'),
148
+ );
149
+ }
@@ -0,0 +1,78 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useLoadMoreSentinel.ts
2
+ // — react-aria's private `useLoadMoreSentinel` util, exposed to RAC as
3
+ // 'react-aria/private/utils/useLoadMoreSentinel'). First landed module-locally in
4
+ // ./components/ListBox.ts; hoisted here verbatim once GridList became the second
5
+ // consumer. octane adaptations: private-util slot threading (the trailing `slot`
6
+ // parameter — callers pass their derived sub-slot); explicit dep arrays are
7
+ // preserved verbatim.
8
+ import type { AsyncLoadable, Collection as ICollection, Node } from '@react-types/shared';
9
+
10
+ import { useRef } from 'octane';
11
+
12
+ import { subSlot } from '../internal';
13
+ import { getScrollParent } from './getScrollParent';
14
+ import { useEffectEvent } from './useEffectEvent';
15
+ import { useLayoutEffect } from './useLayoutEffect';
16
+
17
+ type RefObject<T> = { current: T };
18
+
19
+ export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> {
20
+ collection: ICollection<Node<any>>;
21
+ /**
22
+ * The amount of offset from the bottom of your scrollable region that should trigger load more.
23
+ * Uses a percentage value relative to the scroll body's client height. Load more is then
24
+ * triggered when your current scroll position's distance from the bottom of the currently loaded
25
+ * list of items is less than or equal to the provided value. (e.g. 1 = 100% of the scroll
26
+ * region's height).
27
+ *
28
+ * @default 1
29
+ */
30
+ scrollOffset?: number;
31
+ }
32
+
33
+ export function useLoadMoreSentinel(
34
+ props: LoadMoreSentinelProps,
35
+ ref: RefObject<HTMLElement | null>,
36
+ slot: symbol | undefined,
37
+ ): void {
38
+ let { collection, onLoadMore, scrollOffset = 1 } = props;
39
+
40
+ let sentinelObserver = useRef<IntersectionObserver | null>(null, subSlot(slot, 'observer'));
41
+
42
+ let triggerLoadMore = useEffectEvent(
43
+ (entries: IntersectionObserverEntry[]) => {
44
+ // Use "isIntersecting" over an equality check of 0 since it seems like there is cases where
45
+ // a intersection ratio of 0 can be reported when isIntersecting is actually true
46
+ for (let entry of entries) {
47
+ // Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed.
48
+ // Up to user discretion as to how to handle these multiple onLoadMore calls
49
+ if (entry.isIntersecting && onLoadMore) {
50
+ onLoadMore();
51
+ }
52
+ }
53
+ },
54
+ subSlot(slot, 'trigger'),
55
+ );
56
+
57
+ useLayoutEffect(
58
+ () => {
59
+ if (ref.current) {
60
+ // Tear down and set up a new IntersectionObserver when the collection changes so that we can properly trigger additional loadMores if there is room for more items
61
+ // Need to do this tear down and set up since using a large rootMargin will mean the observer's callback isn't called even when scrolling the item into view beause its visibility hasn't actually changed
62
+ sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {
63
+ root: getScrollParent(ref?.current) as HTMLElement,
64
+ rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`,
65
+ });
66
+ sentinelObserver.current.observe(ref.current);
67
+ }
68
+
69
+ return () => {
70
+ if (sentinelObserver.current) {
71
+ sentinelObserver.current.disconnect();
72
+ }
73
+ };
74
+ },
75
+ [collection, ref, scrollOffset],
76
+ subSlot(slot, 'observe'),
77
+ );
78
+ }
@@ -0,0 +1,123 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/utils/useViewportSize.ts).
2
+ // octane adaptations: hooks come from 'octane'; the public hook gets the binding's slot
3
+ // threading (splitSlot/subSlot); the explicit `[]` dep array on the listener effect is
4
+ // preserved verbatim.
5
+ import { useEffect, useState } from 'octane';
6
+
7
+ import { getActiveElement, getEventTarget } from './shadowdom/DOMFunctions';
8
+ import { isIOS } from './platform';
9
+ import { S, splitSlot, subSlot } from '../internal';
10
+ import { useIsSSR } from '../ssr/SSRProvider';
11
+ import { willOpenKeyboard } from './keyboard';
12
+
13
+ interface ViewportSize {
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ let visualViewport = typeof document !== 'undefined' && window.visualViewport;
19
+
20
+ export function useViewportSize(): ViewportSize;
21
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
22
+ export function useViewportSize(slot: symbol | undefined): ViewportSize;
23
+ export function useViewportSize(...args: any[]): ViewportSize {
24
+ const [, slotArg] = splitSlot(args);
25
+ const slot = slotArg ?? S('useViewportSize');
26
+
27
+ let isSSR = useIsSSR(subSlot(slot, 'ssr'));
28
+ let [size, setSize] = useState(
29
+ () => (isSSR ? { width: 0, height: 0 } : getViewportSize()),
30
+ subSlot(slot, 'size'),
31
+ );
32
+
33
+ useEffect(
34
+ () => {
35
+ let updateSize = (newSize: ViewportSize) => {
36
+ setSize((size: ViewportSize) => {
37
+ if (newSize.width === size.width && newSize.height === size.height) {
38
+ return size;
39
+ }
40
+ return newSize;
41
+ });
42
+ };
43
+
44
+ // Use visualViewport api to track available height even on iOS virtual keyboard opening
45
+ let onResize = () => {
46
+ // Ignore updates when zoomed.
47
+ if (visualViewport && visualViewport.scale > 1) {
48
+ return;
49
+ }
50
+
51
+ updateSize(getViewportSize());
52
+ };
53
+
54
+ // When closing the keyboard, iOS does not fire the visual viewport resize event until the animation is complete.
55
+ // We can anticipate this and resize early by handling the blur event and using the layout size.
56
+ let frame: number;
57
+ let onBlur = (e: FocusEvent) => {
58
+ if (visualViewport && visualViewport.scale > 1) {
59
+ return;
60
+ }
61
+
62
+ if (willOpenKeyboard(getEventTarget(e) as Element)) {
63
+ // Wait one frame to see if a new element gets focused.
64
+ frame = requestAnimationFrame(() => {
65
+ let activeElement = getActiveElement();
66
+ if (!activeElement || !willOpenKeyboard(activeElement)) {
67
+ updateSize({
68
+ width: document.documentElement.clientWidth,
69
+ height: document.documentElement.clientHeight,
70
+ });
71
+ }
72
+ });
73
+ }
74
+ };
75
+
76
+ updateSize(getViewportSize());
77
+
78
+ if (isIOS()) {
79
+ window.addEventListener('blur', onBlur, true);
80
+ }
81
+
82
+ if (!visualViewport) {
83
+ window.addEventListener('resize', onResize);
84
+ } else {
85
+ visualViewport.addEventListener('resize', onResize);
86
+ }
87
+
88
+ return () => {
89
+ cancelAnimationFrame(frame);
90
+ if (isIOS()) {
91
+ window.removeEventListener('blur', onBlur, true);
92
+ }
93
+ if (!visualViewport) {
94
+ window.removeEventListener('resize', onResize);
95
+ } else {
96
+ visualViewport.removeEventListener('resize', onResize);
97
+ }
98
+ };
99
+ },
100
+ [],
101
+ subSlot(slot, 'listen'),
102
+ );
103
+
104
+ return size;
105
+ }
106
+
107
+ /**
108
+ * Get the viewport size without the scrollbar.
109
+ */
110
+ function getViewportSize(): ViewportSize {
111
+ return {
112
+ // Multiply by the visualViewport scale to get the "natural" size, unaffected by pinch zooming.
113
+ width: visualViewport
114
+ ? // The visual viewport width may include the scrollbar gutter. We should use the minimum width between
115
+ // the visual viewport and the document element to ensure that the scrollbar width is always excluded.
116
+ // See: https://github.com/w3c/csswg-drafts/issues/8099
117
+ Math.min(visualViewport.width * visualViewport.scale, document.documentElement.clientWidth)
118
+ : document.documentElement.clientWidth,
119
+ height: visualViewport
120
+ ? visualViewport.height * visualViewport.scale
121
+ : document.documentElement.clientHeight,
122
+ };
123
+ }