Skip to content

Commit

Permalink
Merge branch 'main' into fix/ai-label-rename-classes
Browse files Browse the repository at this point in the history
  • Loading branch information
ariellalgilmore authored Nov 7, 2024
2 parents c4a7794 + cdc8db7 commit 2bf10b2
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 76 deletions.
215 changes: 141 additions & 74 deletions packages/react/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React, {
useCallback,
useContext,
useState,
FocusEvent,
Expand All @@ -20,6 +21,7 @@ import {
UseSelectInterface,
UseSelectProps,
UseSelectState,
UseSelectStateChange,
UseSelectStateChangeTypes,
} from 'downshift';
import cx from 'classnames';
Expand Down Expand Up @@ -240,14 +242,49 @@ export interface DropdownProps<ItemType>

export type DropdownTranslationKey = ListBoxMenuIconTranslationKey;

/**
* Custom state reducer for `useSelect` in Downshift, providing control over
* state changes.
*
* This function is called each time `useSelect` updates its internal state or
* triggers `onStateChange`. It allows for fine-grained control of state
* updates by modifying or overriding the default changes from Downshift's
* reducer.
* https://github.com/downshift-js/downshift/tree/master/src/hooks/useSelect#statereducer
*
* @param {Object} state - The current full state of the Downshift component.
* @param {Object} actionAndChanges - Contains the action type and proposed
* changes from the default Downshift reducer.
* @param {Object} actionAndChanges.changes - Suggested state changes.
* @param {string} actionAndChanges.type - The action type for the state
* change (e.g., item selection).
* @returns {Object} - The modified state based on custom logic or default
* changes if no custom logic applies.
*/
function stateReducer(state, actionAndChanges) {
const { changes, type } = actionAndChanges;

switch (type) {
case ItemMouseMove:
case MenuMouseLeave:
if (changes.highlightedIndex === state.highlightedIndex) {
// Prevent state update if highlightedIndex hasn't changed
return state;
}
return changes;
default:
return changes;
}
}

const Dropdown = React.forwardRef(
<ItemType,>(
{
autoAlign = false,
className: containerClassName,
disabled = false,
direction = 'bottom',
items,
items: itemsProp,
label,
['aria-label']: ariaLabel,
ariaLabel: deprecatedAriaLabel,
Expand Down Expand Up @@ -329,22 +366,33 @@ const Dropdown = React.forwardRef(
const prefix = usePrefix();
const { isFluid } = useContext(FormContext);

const selectProps: UseSelectProps<ItemType> = {
items,
itemToString,
initialSelectedItem,
onSelectedItemChange,
stateReducer,
isItemDisabled(item, _index) {
const isObject = item !== null && typeof item === 'object';
return isObject && 'disabled' in item && item.disabled === true;
const onSelectedItemChange = useCallback(
({ selectedItem }: Partial<UseSelectState<ItemType>>) => {
if (onChange) {
onChange({ selectedItem: selectedItem ?? null });
}
},
onHighlightedIndexChange: ({ highlightedIndex }) => {
if (highlightedIndex! > -1 && typeof window !== undefined) {
[onChange]
);

const isItemDisabled = useCallback((item, _index) => {
const isObject = item !== null && typeof item === 'object';
return isObject && 'disabled' in item && item.disabled === true;
}, []);

const onHighlightedIndexChange = useCallback(
(changes: UseSelectStateChange<ItemType>) => {
const { highlightedIndex } = changes;

if (
highlightedIndex !== undefined &&
highlightedIndex > -1 &&
typeof window !== undefined
) {
const itemArray = document.querySelectorAll(
`li.${prefix}--list-box__menu-item[role="option"]`
);
const highlightedItem = itemArray[highlightedIndex!];
const highlightedItem = itemArray[highlightedIndex];
if (highlightedItem) {
highlightedItem.scrollIntoView({
behavior: 'smooth',
Expand All @@ -353,20 +401,33 @@ const Dropdown = React.forwardRef(
}
}
},
...downshiftProps,
};
const dropdownInstanceId = useId();

function stateReducer(state, actionAndChanges) {
const { changes, type } = actionAndChanges;
[prefix]
);

switch (type) {
case ItemMouseMove:
case MenuMouseLeave:
return { ...changes, highlightedIndex: state.highlightedIndex };
}
return changes;
}
const items = useMemo(() => itemsProp, [itemsProp]);
const selectProps = useMemo(
() => ({
items,
itemToString,
initialSelectedItem,
onSelectedItemChange,
stateReducer,
isItemDisabled,
onHighlightedIndexChange,
...downshiftProps,
}),
[
items,
itemToString,
initialSelectedItem,
onSelectedItemChange,
stateReducer,
isItemDisabled,
onHighlightedIndexChange,
downshiftProps,
]
);
const dropdownInstanceId = useId();

// only set selectedItem if the prop is defined. Setting if it is undefined
// will overwrite default selected items from useSelect
Expand Down Expand Up @@ -432,9 +493,13 @@ const Dropdown = React.forwardRef(

// needs to be Capitalized for react to render it correctly
const ItemToElement = itemToElement;
const toggleButtonProps = getToggleButtonProps({
'aria-label': ariaLabel || deprecatedAriaLabel,
});
const toggleButtonProps = useMemo(
() =>
getToggleButtonProps({
'aria-label': ariaLabel || deprecatedAriaLabel,
}),
[getToggleButtonProps, ariaLabel, deprecatedAriaLabel, isOpen]
);

const helper =
helperText && !isFluid ? (
Expand All @@ -443,14 +508,6 @@ const Dropdown = React.forwardRef(
</div>
) : null;

function onSelectedItemChange({
selectedItem,
}: Partial<UseSelectState<ItemType>>) {
if (onChange) {
onChange({ selectedItem: selectedItem ?? null });
}
}

const handleFocus = (evt: FocusEvent<HTMLDivElement>) => {
setIsFocused(evt.type === 'focus' ? true : false);
};
Expand All @@ -459,11 +516,39 @@ const Dropdown = React.forwardRef(

const [currTimer, setCurrTimer] = useState<NodeJS.Timeout>();

// eslint-disable-next-line prefer-const
let [isTyping, setIsTyping] = useState(false);
const [isTyping, setIsTyping] = useState(false);

const onKeyDownHandler = useCallback(
(evt: React.KeyboardEvent<HTMLButtonElement>) => {
if (
evt.code !== 'Space' ||
!['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(evt.key)
) {
setIsTyping(true);
}
if (
(isTyping && evt.code === 'Space') ||
!['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(evt.key)
) {
if (currTimer) {
clearTimeout(currTimer);
}
setCurrTimer(
setTimeout(() => {
setIsTyping(false);
}, 3000)
);
}
if (toggleButtonProps.onKeyDown) {
toggleButtonProps.onKeyDown(evt);
}
},
[isTyping, currTimer, toggleButtonProps]
);

const readOnlyEventHandlers = readOnly
? {
const readOnlyEventHandlers = useMemo(() => {
if (readOnly) {
return {
onClick: (evt: MouseEvent<HTMLButtonElement>) => {
// NOTE: does not prevent click
evt.preventDefault();
Expand All @@ -477,49 +562,31 @@ const Dropdown = React.forwardRef(
evt.preventDefault();
}
},
}
: {
onKeyDown: (evt: React.KeyboardEvent<HTMLButtonElement>) => {
if (
evt.code !== 'Space' ||
!['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(evt.key)
) {
setIsTyping(true);
}
if (
(isTyping && evt.code === 'Space') ||
!['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(evt.key)
) {
if (currTimer) {
clearTimeout(currTimer);
}
setCurrTimer(
setTimeout(() => {
setIsTyping(false);
}, 3000)
);
}
if (toggleButtonProps.onKeyDown) {
toggleButtonProps.onKeyDown(evt);
}
},
};
} else {
return {
onKeyDown: onKeyDownHandler,
};
}
}, [readOnly, onKeyDownHandler]);

const menuProps = useMemo(
() =>
getMenuProps({
ref: enableFloatingStyles || autoAlign ? refs.setFloating : null,
}),
[autoAlign, getMenuProps, refs.setFloating]
[autoAlign, getMenuProps, refs.setFloating, enableFloatingStyles]
);

// Slug is always size `mini`
let normalizedSlug;
if (slug && slug['type']?.displayName === 'AILabel') {
normalizedSlug = React.cloneElement(slug as React.ReactElement<any>, {
size: 'mini',
});
}
const normalizedSlug = useMemo(() => {
if (slug && slug['type']?.displayName === 'AILabel') {
return React.cloneElement(slug as React.ReactElement<any>, {
size: 'mini',
});
}
return slug;
}, [slug]);

return (
<div className={wrapperClasses} {...other}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,37 @@
*/

import PropTypes from 'prop-types';
import React from 'react';
import React, { ElementType, ReactNode } from 'react';
import { LayoutDirectionContext } from './LayoutDirectionContext';

type Direction = 'ltr' | 'rtl';

interface LayoutDirectionContextValue {
direction: Direction;
}

interface LayoutDirectionProps {
/**
* Customize the element type used to render the outermost node
*/
as?: ElementType;
/**
* Provide child elements or components to be rendered inside of this
* component
*/
children?: ReactNode;
/**
* Specify the layout direction of this part of the page
*/
dir: Direction;
}

function LayoutDirection({
as: BaseComponent = 'div',
children,
dir,
...rest
}) {
}: LayoutDirectionProps) {
const value = React.useMemo(() => {
return {
direction: dir,
Expand Down

0 comments on commit 2bf10b2

Please sign in to comment.