Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
import {AriaTextFieldProps} from '@react-aria/textfield';
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
import {getInteractionModality} from '@react-aria/interactions';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef} from 'react';
import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useLocalizedStringFormatter} from '@react-aria/i18n';

export interface CollectionOptions extends DOMProps, AriaLabelingProps {
Expand Down Expand Up @@ -88,7 +88,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
disableVirtualFocus = false
} = props;

let collectionId = useSlotId();
let collectionId = useId();
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
let delayNextActiveDescendant = useRef(false);
let queuedActiveDescendant = useRef<string | null>(null);
Expand All @@ -97,7 +97,11 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
// For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
// moving focus back to the subtriggers
let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
let shouldUseVirtualFocus = !isMobileScreenReader && !disableVirtualFocus;
let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(!isMobileScreenReader && !disableVirtualFocus);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still want to keep disableVirtualFocus as a prop so a user can opt out of the virtual focus behavior based on their use case

// Tracks if a collection has been connected to the autocomplete. If false, we don't want to add various attributes to the autocomplete input
// since it isn't attached to a filterable collection (e.g. Tabs)
let [hasCollection, setHasCollection] = useState(false);

useEffect(() => {
return () => clearTimeout(timeout.current);
}, []);
Expand Down Expand Up @@ -145,8 +149,16 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
lastCollectionNode.current = collectionNode;
collectionNode.addEventListener('focusin', updateActiveDescendant);
// If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it
// that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless
// of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props.
if (collectionNode.getAttribute('tabindex') != null) {
setShouldUseVirtualFocus(false);
}
setHasCollection(true);
} else {
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
setHasCollection(false);
}
}, [updateActiveDescendant]);

Expand Down Expand Up @@ -393,7 +405,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
onFocus
};

if (collectionId) {
if (hasCollection) {
inputProps = {
...inputProps,
...(shouldUseVirtualFocus && virtualFocusProps),
Expand All @@ -413,7 +425,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
inputProps,
collectionProps: mergeProps(collectionProps, {
shouldUseVirtualFocus,
disallowTypeAhead: true
disallowTypeAhead: shouldUseVirtualFocus
}),
collectionRef: mergedCollectionRef,
filter: filter != null ? filterFn : undefined
Expand Down
89 changes: 47 additions & 42 deletions packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import intlMessages from '../intl/*.json';
import {mergeRefs, useResizeObserver, useSlotId} from '@react-aria/utils';
import {Node} from 'react-stately';
import {Placement} from 'react-aria';
import {PopoverBase} from './Popover';
import {Popover} from './Popover';
import {pressScale} from './pressScale';
import {ProgressCircle} from './ProgressCircle';
import {TextFieldRef} from '@react-types/textfield';
Expand Down Expand Up @@ -478,7 +478,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
}

let triggerRef = useRef<HTMLDivElement>(null);
// Make menu width match input + button
// Make menu width match input + button
let [triggerWidth, setTriggerWidth] = useState<string | null>(null);
let onResize = useCallback(() => {
if (triggerRef.current) {
Expand Down Expand Up @@ -643,57 +643,62 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
description={descriptionMessage}>
{errorMessage}
</HelpText>
<PopoverBase
<Popover
hideArrow
triggerRef={triggerRef}
offset={menuOffset}
placement={`${direction} ${align}` as Placement}
shouldFlip={shouldFlip}
UNSAFE_style={{
width: menuWidth ? `${menuWidth}px` : undefined,
// manually subtract border as we can't set Popover to border-box, it causes the contents to spill out
'--trigger-width': `calc(${triggerWidth} - 2px)`
'--trigger-width': (menuWidth ? menuWidth + 'px' : triggerWidth)
} as CSSProperties}
padding="none"
styles={style({
minWidth: '--trigger-width',
width: '--trigger-width'
})}>
<Provider
values={[
[HeaderContext, {styles: listboxHeader({size})}],
[HeadingContext, {
// @ts-ignore
role: 'presentation',
styles: sectionHeading
}],
[TextContext, {
slots: {
'description': {styles: description({size})}
}
}]
]}>
<Virtualizer
layout={ListLayout}
layoutOptions={{
estimatedRowHeight: 32,
padding: 8,
estimatedHeadingHeight: 50,
loaderHeight: LOADER_ROW_HEIGHTS[size][scale]
}}>
<ListBox
dependencies={props.dependencies}
renderEmptyState={() => (
<span className={emptyStateText({size})}>
{loadingState === 'loading' ? stringFormatter.format('table.loading') : stringFormatter.format('combobox.noResults')}
</span>
)}
items={items}
className={listbox({size})}>
{renderer}
</ListBox>
</Virtualizer>
</Provider>
</PopoverBase>
<div
className={style({
display: 'flex',
size: 'full'
})}>
<Provider
values={[
[HeaderContext, {styles: listboxHeader({size})}],
[HeadingContext, {
// @ts-ignore
role: 'presentation',
styles: sectionHeading
}],
[TextContext, {
slots: {
'description': {styles: description({size})}
}
}]
]}>
<Virtualizer
layout={ListLayout}
layoutOptions={{
estimatedRowHeight: 32,
padding: 8,
estimatedHeadingHeight: 50,
loaderHeight: LOADER_ROW_HEIGHTS[size][scale]
}}>
<ListBox
dependencies={props.dependencies}
renderEmptyState={() => (
<span className={emptyStateText({size})}>
{loadingState === 'loading' ? stringFormatter.format('table.loading') : stringFormatter.format('combobox.noResults')}
</span>
)}
items={items}
className={listbox({size})}>
{renderer}
</ListBox>
</Virtualizer>
</Provider>
</div>
</Popover>
</InternalComboboxContext.Provider>
</>
);
Expand Down
68 changes: 36 additions & 32 deletions packages/@react-spectrum/s2/src/ContextualHelp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import InfoIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {mergeStyles} from '../style/runtime';
import {PopoverBase, PopoverDialogProps} from './Popover';
import {Popover, PopoverDialogProps} from './Popover';
import {space, style} from '../style' with {type: 'macro'};
import {StyleProps} from './style-utils' with { type: 'macro' };
import {useLocalizedStringFormatter} from '@react-aria/i18n';
Expand Down Expand Up @@ -39,11 +39,12 @@ export interface ContextualHelpProps extends
size?: 'XS' | 'S'
}

const popover = style({
fontFamily: 'sans',
const wrappingDiv = style({
minWidth: 268,
width: 268,
padding: 24
padding: 24,
boxSizing: 'border-box',
height: 'full'
});

export const ContextualHelpContext = createContext<ContextValue<Partial<ContextualHelpProps>, FocusableRefValue<HTMLButtonElement>>>(null);
Expand Down Expand Up @@ -96,39 +97,42 @@ export const ContextualHelp = forwardRef(function ContextualHelp(props: Contextu
isQuiet>
{variant === 'info' ? <InfoIcon /> : <HelpIcon />}
</ActionButton>
<PopoverBase
<Popover
padding="none"
placement={placement}
shouldFlip={shouldFlip}
// not working => containerPadding={containerPadding}
offset={offset}
crossOffset={crossOffset}
hideArrow
styles={popover}>
<RACDialog className={mergeStyles(dialogInner, style({borderRadius: 'none', margin: 'calc(self(paddingTop) * -1)', padding: 24}))}>
<Provider
values={[
[TextContext, {
slots: {
[DEFAULT_SLOT]: {}
}
}],
[HeadingContext, {styles: style({
font: 'heading-xs',
margin: 0,
marginBottom: space(8) // This only makes it 10px on mobile and should be 12px
})}],
[ContentContext, {styles: style({
font: 'body-sm'
})}],
[FooterContext, {styles: style({
font: 'body-sm',
marginTop: 16
})}]
]}>
{children}
</Provider>
</RACDialog>
</PopoverBase>
hideArrow>
<div
className={wrappingDiv}>
<RACDialog className={mergeStyles(dialogInner, style({borderRadius: 'none', margin: 'calc(self(paddingTop) * -1)', padding: 24}))}>
<Provider
values={[
[TextContext, {
slots: {
[DEFAULT_SLOT]: {}
}
}],
[HeadingContext, {styles: style({
font: 'heading-xs',
margin: 0,
marginBottom: space(8) // This only makes it 10px on mobile and should be 12px
})}],
[ContentContext, {styles: style({
font: 'body-sm'
})}],
[FooterContext, {styles: style({
font: 'body-sm',
marginTop: 16
})}]
]}>
{children}
</Provider>
</RACDialog>
</div>
</Popover>
</DialogTrigger>
);
});
43 changes: 24 additions & 19 deletions packages/@react-spectrum/s2/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {FieldGroup, FieldLabel, HelpText} from './Field';
import {forwardRefType, GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {PopoverBase} from './Popover';
import {Popover} from './Popover';
import {pressScale} from './pressScale';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand Down Expand Up @@ -237,25 +237,30 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function

export function CalendarPopover(props: PropsWithChildren): ReactElement {
return (
<PopoverBase
<Popover
hideArrow
styles={style({
paddingX: 16,
paddingY: 32,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 16
})}>
<Dialog>
<Provider
values={[
[OverlayTriggerStateContext, null]
]}>
{props.children}
</Provider>
</Dialog>
</PopoverBase>
padding="none">
<div
className={style({
paddingX: 16,
paddingY: 32,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 16,
boxSizing: 'border-box',
size: 'full'
})}>
<Dialog>
<Provider
values={[
[OverlayTriggerStateContext, null]
]}>
{props.children}
</Provider>
</Dialog>
</div>
</Popover>
);
}

Expand Down
23 changes: 15 additions & 8 deletions packages/@react-spectrum/s2/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {forwardRefType} from './types';
import {HeaderContext, HeadingContext, KeyboardContext, Text, TextContext} from './Content';
import {IconContext} from './Icon'; // chevron right removed??
import {ImageContext} from './Image';
import {InPopoverContext, PopoverBase, PopoverContext} from './Popover';
import {InPopoverContext, Popover, PopoverContext} from './Popover';
import LinkOutIcon from '../ui-icons/LinkOut';
import {mergeStyles} from '../style/runtime';
import {Placement, useLocale} from 'react-aria';
Expand Down Expand Up @@ -320,6 +320,11 @@ let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu

let InternalMenuTriggerContext = createContext<Omit<MenuTriggerProps, 'children'> | null>(null);

let wrappingDiv = style({
display: 'flex',
size: 'full'
});

/**
* Menus display a list of actions or options that a user can choose.
*/
Expand Down Expand Up @@ -366,14 +371,16 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu<T

if (isPopover) {
return (
<PopoverBase
<Popover
ref={ref}
hideArrow
UNSAFE_style={UNSAFE_style}
UNSAFE_className={UNSAFE_className}
styles={styles}>
{content}
</PopoverBase>
padding="none"
hideArrow>
<div
style={UNSAFE_style}
className={(UNSAFE_className || '') + wrappingDiv}>
{content}
</div>
</Popover>
);
}

Expand Down
Loading