Skip to content

Commit b01500b

Browse files
authored
Support links in collection components (#4993)
1 parent f6239e3 commit b01500b

File tree

82 files changed

+1962
-294
lines changed

Some content is hidden

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

82 files changed

+1962
-294
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ governing permissions and limitations under the License.
7777
.spectrum-Menu-item {
7878
cursor: default;
7979
position: relative;
80+
display: block;
8081

8182
box-sizing: border-box;
8283

@@ -89,10 +90,18 @@ governing permissions and limitations under the License.
8990
font-style: normal;
9091
text-decoration: none;
9192

93+
&[href] {
94+
cursor: pointer;
95+
}
96+
9297
&:focus {
9398
outline: none;
9499
}
95100

101+
&.is-disabled {
102+
cursor: default;
103+
}
104+
96105
&.is-selected {
97106
.spectrum-Menu-checkmark {
98107
display: block;

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ governing permissions and limitations under the License.
6868
cursor: default;
6969
outline: none;
7070

71+
&[href] {
72+
cursor: pointer;
73+
}
74+
7175
&.is-disabled {
7276
cursor: default;
73-
74-
.spectrum-Tabs-itemLabel {
75-
cursor: default;
76-
}
7777
}
7878

7979
.spectrum-Icon {
@@ -107,7 +107,6 @@ governing permissions and limitations under the License.
107107
}
108108

109109
.spectrum-Tabs-itemLabel {
110-
cursor: default;
111110
vertical-align: top;
112111
display: inline-block;
113112

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ governing permissions and limitations under the License.
4848
align-items: center;
4949
box-sizing: border-box;
5050
position: relative;
51+
cursor: default;
5152

5253
margin: var(--spectrum-tag-margin);
5354
padding-inline-start: calc(var(--spectrum-tag-padding-x) - var(--spectrum-tag-border-size));
@@ -65,7 +66,12 @@ governing permissions and limitations under the License.
6566
box-shadow var(--spectrum-global-animation-duration-100) ease-in-out,
6667
background-color var(--spectrum-global-animation-duration-100) ease-in-out;
6768

69+
&[data-href] {
70+
cursor: pointer;
71+
}
72+
6873
&.is-disabled {
74+
cursor: default;
6975
pointer-events: none;
7076
}
7177

@@ -100,7 +106,6 @@ governing permissions and limitations under the License.
100106
margin-inline-end: var(--spectrum-tag-padding-x);
101107
flex: 1 1 auto;
102108
font-size: var(--spectrum-tag-text-size);
103-
cursor: default;
104109
overflow: hidden;
105110
white-space: nowrap;
106111
text-overflow: ellipsis;

packages/@react-aria/accordion/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"@swc/helpers": "^0.5.0"
3434
},
3535
"peerDependencies": {
36-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
36+
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
37+
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
3738
},
3839
"publishConfig": {
3940
"access": "public"

packages/@react-aria/actiongroup/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"@swc/helpers": "^0.5.0"
3535
},
3636
"peerDependencies": {
37-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
37+
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0",
38+
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
3839
},
3940
"publishConfig": {
4041
"access": "public"

packages/@react-aria/combobox/src/useComboBox.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {AriaComboBoxProps} from '@react-types/combobox';
1616
import {ariaHideOutside} from '@react-aria/overlays';
1717
import {AriaListBoxOptions, getItemId, listData} from '@react-aria/listbox';
1818
import {BaseEvent, DOMAttributes, KeyboardDelegate, PressEvent} from '@react-types/shared';
19-
import {chain, isAppleDevice, mergeProps, useLabels} from '@react-aria/utils';
19+
import {chain, isAppleDevice, mergeProps, useLabels, useRouter} from '@react-aria/utils';
2020
import {ComboBoxState} from '@react-stately/combobox';
2121
import {FocusEvent, InputHTMLAttributes, KeyboardEvent, RefObject, TouchEvent, useEffect, useMemo, useRef} from 'react';
2222
import {getChildNodes, getItemCount} from '@react-stately/collections';
@@ -106,6 +106,8 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
106106
isVirtualized: true
107107
});
108108

109+
let router = useRouter();
110+
109111
// For textfield specific keydown operations
110112
let onKeyDown = (e: BaseEvent<KeyboardEvent<any>>) => {
111113
switch (e.key) {
@@ -116,7 +118,19 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
116118
e.preventDefault();
117119
}
118120

119-
state.commit();
121+
// If the focused item is a link, trigger opening it. Items that are links are not selectable.
122+
if (state.isOpen && state.selectionManager.focusedKey != null && state.selectionManager.isLink(state.selectionManager.focusedKey)) {
123+
if (e.key === 'Enter') {
124+
let item = listBoxRef.current.querySelector(`[data-key="${state.selectionManager.focusedKey}"]`);
125+
if (item instanceof HTMLAnchorElement) {
126+
router.open(item, e);
127+
}
128+
}
129+
130+
state.close();
131+
} else {
132+
state.commit();
133+
}
120134
break;
121135
case 'Escape':
122136
if (
@@ -330,7 +344,8 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
330344
autoFocus: state.focusStrategy,
331345
shouldUseVirtualFocus: true,
332346
shouldSelectOnPressUp: true,
333-
shouldFocusOnHover: true
347+
shouldFocusOnHover: true,
348+
linkBehavior: 'selection' as const
334349
}),
335350
descriptionProps,
336351
errorMessageProps

packages/@react-aria/gridlist/src/useGridList.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ export interface AriaGridListOptions<T> extends Omit<AriaGridListProps<T>, 'chil
5151
* Whether focus should wrap around when the end/start is reached.
5252
* @default false
5353
*/
54-
shouldFocusWrap?: boolean
54+
shouldFocusWrap?: boolean,
55+
/**
56+
* The behavior of links in the collection.
57+
* - 'action': link behaves like onAction.
58+
* - 'selection': link follows selection interactions (e.g. if URL drives selection).
59+
* - 'override': links override all other interactions (link items are not selectable).
60+
* @default 'action'
61+
*/
62+
linkBehavior?: 'action' | 'selection' | 'override'
5563
}
5664

5765
export interface GridListAria {
@@ -70,7 +78,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
7078
let {
7179
isVirtualized,
7280
keyboardDelegate,
73-
onAction
81+
onAction,
82+
linkBehavior = 'action'
7483
} = props;
7584

7685
if (!props['aria-label'] && !props['aria-labelledby']) {
@@ -85,11 +94,12 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
8594
keyboardDelegate: keyboardDelegate,
8695
isVirtualized,
8796
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
88-
shouldFocusWrap: props.shouldFocusWrap
97+
shouldFocusWrap: props.shouldFocusWrap,
98+
linkBehavior
8999
});
90100

91101
let id = useId(props.id);
92-
listMap.set(state, {id, onAction});
102+
listMap.set(state, {id, onAction, linkBehavior});
93103

94104
let descriptionProps = useHighlightSelectionDescription({
95105
selectionManager: state.selectionManager,

packages/@react-aria/gridlist/src/useGridListItem.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {DOMAttributes, FocusableElement, Node as RSNode} from '@react-types/shared';
1414
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
1515
import {getRowId, listMap} from './utils';
16-
import {getScrollParent, mergeProps, scrollIntoViewport, useSlotId} from '@react-aria/utils';
16+
import {getScrollParent, getSyntheticLinkProps, mergeProps, scrollIntoViewport, useSlotId} from '@react-aria/utils';
1717
import {isFocusVisible} from '@react-aria/interactions';
1818
import type {ListState} from '@react-stately/list';
1919
import {KeyboardEvent as ReactKeyboardEvent, RefObject, useRef} from 'react';
@@ -53,7 +53,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
5353
} = props;
5454

5555
let {direction} = useLocale();
56-
let {onAction} = listMap.get(state);
56+
let {onAction, linkBehavior} = listMap.get(state);
5757
let descriptionId = useSlotId();
5858

5959
// We need to track the key of the item at the time it was last focused so that we force
@@ -77,7 +77,8 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
7777
isVirtualized,
7878
shouldSelectOnPressUp,
7979
onAction: onAction ? () => onAction(node.key) : undefined,
80-
focus
80+
focus,
81+
linkBehavior
8182
});
8283

8384
let onKeyDown = (e: ReactKeyboardEvent) => {
@@ -177,7 +178,8 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
177178
}
178179
};
179180

180-
let rowProps: DOMAttributes = mergeProps(itemProps, {
181+
let linkProps = getSyntheticLinkProps(node.props);
182+
let rowProps: DOMAttributes = mergeProps(itemProps, linkProps, {
181183
role: 'row',
182184
onKeyDownCapture: onKeyDown,
183185
onFocus,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import type {ListState} from '@react-stately/list';
1515

1616
interface ListMapShared {
1717
id: string,
18-
onAction: (key: Key) => void
18+
onAction: (key: Key) => void,
19+
linkBehavior?: 'action' | 'selection' | 'override'
1920
}
2021

2122
// Used to share:

0 commit comments

Comments
 (0)