Skip to content

Commit e9eb1c1

Browse files
committed
Merge branch 'main' of github.com:adobe/react-spectrum into table-column-updating
# Conflicts: # packages/@react-aria/collections/src/Document.ts
2 parents d383087 + e9bd3a3 commit e9eb1c1

File tree

89 files changed

+3232
-646
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+3232
-646
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"@storybook/addon-themes": "^7.6.19",
114114
"@storybook/api": "^7.6.19",
115115
"@storybook/components": "^7.6.19",
116+
"@storybook/jest": "^0.2.3",
116117
"@storybook/manager-api": "^7.6.19",
117118
"@storybook/preview": "^7.6.19",
118119
"@storybook/preview-api": "^7.6.19",

packages/@adobe/spectrum-css-temp/components/button/index.css

+18
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,24 @@ a.spectrum-ActionButton {
372372
margin-block: 0;
373373
margin-inline: auto;
374374
}
375+
376+
&.spectrum-ClearButton--inset.spectrum-ClearButton--inset {
377+
transition: unset;
378+
box-sizing: border-box;
379+
&:after {
380+
transition: unset;
381+
left: 4px;
382+
right: 4px;
383+
bottom: 4px;
384+
top: 4px;
385+
}
386+
387+
&:focus-visible {
388+
&:after {
389+
box-shadow: inset 0 0 0 var(--spectrum-focus-ring-size) var(--spectrum-focus-ring-color);
390+
}
391+
}
392+
}
375393
}
376394

377395
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {

packages/@react-aria/collections/src/CollectionBuilder.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ function useSSRCollectionNode<T extends Element>(Type: string, props: object, re
157157
return <Type ref={itemRef}>{children}</Type>;
158158
}
159159

160-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement): (props: P & React.RefAttributes<T>) => ReactElement | null;
161-
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement): (props: P & React.RefAttributes<T>) => ReactElement | null;
162-
export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement): (props: P & React.RefAttributes<any>) => ReactElement | null {
160+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
161+
export function createLeafComponent<T extends object, P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null): (props: P & React.RefAttributes<T>) => ReactElement | null;
162+
export function createLeafComponent<P extends object, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node?: any) => ReactElement | null): (props: P & React.RefAttributes<any>) => ReactElement | null {
163163
let Component = ({node}) => render(node.props, node.props.ref, node);
164164
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
165165
let focusableProps = useContext(FocusableContext);
@@ -190,7 +190,7 @@ export function createLeafComponent<P extends object, E extends Element>(type: s
190190
return Result;
191191
}
192192

193-
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
193+
export function createBranchComponent<T extends object, P extends {children?: any}, E extends Element>(type: string, render: (props: P, ref: ForwardedRef<E>, node: Node<T>) => ReactElement | null, useChildren: (props: P) => ReactNode = useCollectionChildren): (props: P & React.RefAttributes<E>) => ReactElement | null {
194194
let Component = ({node}) => render(node.props, node.props.ref, node);
195195
let Result = (forwardRef as forwardRefType)((props: P, ref: ForwardedRef<E>) => {
196196
let children = useChildren(props);

packages/@react-aria/collections/src/Document.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ export class BaseNode<T> {
103103
}
104104

105105
private invalidateChildIndices(child: ElementNode<T>): void {
106-
if (this._minInvalidChildIndex == null || child.index < this._minInvalidChildIndex.index) {
106+
if (this._minInvalidChildIndex == null || !this._minInvalidChildIndex.isConnected || child.index < this._minInvalidChildIndex.index) {
107107
this._minInvalidChildIndex = child;
108+
this.ownerDocument.markDirty(this);
108109
}
109110
}
110111

@@ -156,8 +157,11 @@ export class BaseNode<T> {
156157

157158
newNode.nextSibling = referenceNode;
158159
newNode.previousSibling = referenceNode.previousSibling;
159-
newNode.index = referenceNode.index;
160-
160+
// Ensure that the newNode's index is less than that of the reference node so that
161+
// invalidateChildIndices will properly use the newNode as the _minInvalidChildIndex, thus making sure
162+
// we will properly update the indexes of all sibiling nodes after the newNode. The value here doesn't matter
163+
// since updateChildIndices should calculate the proper indexes.
164+
newNode.index = referenceNode.index - 1;
161165
if (this.firstChild === referenceNode) {
162166
this.firstChild = newNode;
163167
} else if (referenceNode.previousSibling) {

packages/@react-aria/tag/docs/useTagGroup.mdx

+25-9
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,14 @@ interface TagProps<T> extends AriaTagProps<T> {
134134
function Tag<T>(props: TagProps<T>) {
135135
let {item, state} = props;
136136
let ref = React.useRef(null);
137-
let {focusProps, isFocusVisible} = useFocusRing({within: true});
137+
let {focusProps, isFocusVisible} = useFocusRing({within: false});
138138
let {rowProps, gridCellProps, removeButtonProps, allowsRemoving} = useTag(props, state, ref);
139139

140140
return (
141141
<div ref={ref} {...rowProps} {...focusProps} data-focus-visible={isFocusVisible}>
142142
<div {...gridCellProps}>
143143
{item.rendered}
144-
{allowsRemoving && <Button {...removeButtonProps}></Button>}
144+
{allowsRemoving && <Button {...removeButtonProps}></Button>}
145145
</div>
146146
</div>
147147
);
@@ -172,13 +172,16 @@ function Tag<T>(props: TagProps<T>) {
172172
}
173173

174174
.tag-group [role="row"] {
175-
display: flex;
176-
align-items: center;
177175
border: 1px solid gray;
176+
forced-color-adjust: none;
178177
border-radius: 4px;
179-
padding: 2px 5px;
180-
cursor: default;
178+
padding: 2px 8px;
179+
font-size: 0.929rem;
181180
outline: none;
181+
cursor: default;
182+
display: flex;
183+
align-items: center;
184+
transition: border-color 200ms;
182185

183186
&[data-focus-visible=true] {
184187
outline: 2px solid slateblue;
@@ -197,13 +200,24 @@ function Tag<T>(props: TagProps<T>) {
197200
}
198201

199202
.tag-group [role="gridcell"] {
200-
margin: 0 5px;
203+
display: contents;
201204
}
202205

203206
.tag-group [role="row"] button {
204207
background: none;
205208
border: none;
206-
padding-right: 0;
209+
padding: 0;
210+
margin-left: 4px;
211+
outline: none;
212+
font-size: 0.95em;
213+
border-radius: 100%;
214+
aspect-ratio: 1/1;
215+
height: 100%;
216+
217+
&[data-focus-visible=true] {
218+
outline: 2px solid slateblue;
219+
outline-offset: -1px;
220+
}
207221
}
208222

209223
.tag-group .description {
@@ -227,11 +241,13 @@ The `Button` component is used in the above example to remove a tag. It is built
227241

228242
```tsx example export=true render=false
229243
import {useButton} from '@react-aria/button';
244+
import {mergeProps} from '@react-aria/utils';
230245

231246
function Button(props) {
232247
let ref = React.useRef(null);
233248
let {buttonProps} = useButton(props, ref);
234-
return <button {...buttonProps} ref={ref}>{props.children}</button>;
249+
let {focusProps, isFocusVisible} = useFocusRing({within: false});
250+
return <button {...mergeProps(buttonProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible}>{props.children}</button>;
235251
}
236252
```
237253

packages/@react-aria/utils/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ export {useEffectEvent} from './useEffectEvent';
4545
export {useDeepMemo} from './useDeepMemo';
4646
export {useFormReset} from './useFormReset';
4747
export {useLoadMore} from './useLoadMore';
48+
export {UNSTABLE_useLoadMoreSentinel} from './useLoadMoreSentinel';
4849
export {inertValue} from './inertValue';
4950
export {CLEAR_FOCUS_EVENT, FOCUS_EVENT} from './constants';
5051
export {isCtrlKeyPressed} from './keyboard';
5152
export {useEnterAnimation, useExitAnimation} from './animation';
5253
export {isFocusable, isTabbable} from './isFocusable';
54+
55+
export type {LoadMoreSentinelProps} from './useLoadMoreSentinel';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import type {AsyncLoadable, Collection, Node} from '@react-types/shared';
14+
import {getScrollParent} from './getScrollParent';
15+
import {RefObject, useRef} from 'react';
16+
import {useEffectEvent} from './useEffectEvent';
17+
import {useLayoutEffect} from './useLayoutEffect';
18+
19+
export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> {
20+
collection: Collection<Node<unknown>>,
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 triggered
24+
* when your current scroll position's distance from the bottom of the currently loaded list of items is less than
25+
* or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
26+
* @default 1
27+
*/
28+
scrollOffset?: number
29+
}
30+
31+
export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void {
32+
let {collection, onLoadMore, scrollOffset = 1} = props;
33+
34+
let sentinelObserver = useRef<IntersectionObserver>(null);
35+
36+
let triggerLoadMore = useEffectEvent((entries: IntersectionObserverEntry[]) => {
37+
// Use "isIntersecting" over an equality check of 0 since it seems like there is cases where
38+
// a intersection ratio of 0 can be reported when isIntersecting is actually true
39+
for (let entry of entries) {
40+
// Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed.
41+
// Up to user discretion as to how to handle these multiple onLoadMore calls
42+
if (entry.isIntersecting && onLoadMore) {
43+
onLoadMore();
44+
}
45+
}
46+
});
47+
48+
useLayoutEffect(() => {
49+
if (ref.current) {
50+
// 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
51+
// 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
52+
// https://codesandbox.io/p/sandbox/magical-swanson-dhgp89?file=%2Fsrc%2FApp.js%3A21%2C21
53+
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: getScrollParent(ref?.current) as HTMLElement, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`});
54+
sentinelObserver.current.observe(ref.current);
55+
}
56+
57+
return () => {
58+
if (sentinelObserver.current) {
59+
sentinelObserver.current.disconnect();
60+
}
61+
};
62+
}, [collection, triggerLoadMore, ref, scrollOffset]);
63+
}

packages/@react-aria/virtualizer/src/ScrollView.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
9898
// Prevent rubber band scrolling from shaking when scrolling out of bounds
9999
state.scrollTop = Math.max(0, Math.min(scrollTop, contentSize.height - state.height));
100100
state.scrollLeft = Math.max(0, Math.min(scrollLeft, contentSize.width - state.width));
101-
102101
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height));
103102

104103
if (!state.isScrolling) {
@@ -199,6 +198,7 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
199198

200199
// Update visible rect when the content size changes, in case scrollbars need to appear or disappear.
201200
let lastContentSize = useRef<Size | null>(null);
201+
let [update, setUpdate] = useState({});
202202
useLayoutEffect(() => {
203203
if (!isUpdatingSize.current && (lastContentSize.current == null || !contentSize.equals(lastContentSize.current))) {
204204
// React doesn't allow flushSync inside effects, so queue a microtask.
@@ -209,7 +209,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
209209
// https://github.com/reactwg/react-18/discussions/102
210210
// @ts-ignore
211211
if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') {
212-
updateSize(fn => fn());
212+
// This is so we update size in a separate render but within the same act. Needs to be setState instead of refs
213+
// due to strict mode.
214+
setUpdate({});
215+
lastContentSize.current = contentSize;
216+
return;
213217
} else {
214218
queueMicrotask(() => updateSize(flushSync));
215219
}
@@ -218,6 +222,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement
218222
lastContentSize.current = contentSize;
219223
});
220224

225+
// Will only run in tests, needs to be in separate effect so it is properly run in the next render in strict mode.
226+
useLayoutEffect(() => {
227+
updateSize(fn => fn());
228+
}, [update]);
229+
221230
let onResize = useCallback(() => {
222231
updateSize(flushSync);
223232
}, [updateSize]);

packages/@react-spectrum/button/src/ClearButton.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ interface ClearButtonProps<T extends ElementType = 'button'> extends ButtonProps
2525
focusClassName?: string,
2626
variant?: 'overBackground',
2727
excludeFromTabOrder?: boolean,
28-
preventFocus?: boolean
28+
preventFocus?: boolean,
29+
inset?: boolean
2930
}
3031

3132
export const ClearButton = React.forwardRef(function ClearButton(props: ClearButtonProps, ref: FocusableRef<HTMLButtonElement>) {
@@ -37,6 +38,7 @@ export const ClearButton = React.forwardRef(function ClearButton(props: ClearBut
3738
isDisabled,
3839
preventFocus,
3940
elementType = preventFocus ? 'div' : 'button' as ElementType,
41+
inset = false,
4042
...otherProps
4143
} = props;
4244
let domRef = useFocusableRef(ref);
@@ -66,7 +68,8 @@ export const ClearButton = React.forwardRef(function ClearButton(props: ClearBut
6668
[`spectrum-ClearButton--${variant}`]: variant,
6769
'is-disabled': isDisabled,
6870
'is-active': isPressed,
69-
'is-hovered': isHovered
71+
'is-hovered': isHovered,
72+
'spectrum-ClearButton--inset': inset
7073
},
7174
styleProps.className
7275
)

0 commit comments

Comments
 (0)