Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Dropdown): support AutoComplete in DropdownHeader #2553

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .github/workflows/blade-interaction-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Blade Interaction Tests

# Runs the action when
# 1. 'Run Interaction Tests' label is added to PR
# Runs the action when
# 1. 'Run Interaction Tests' label is added to PR
# 2. Workflow is trigerred manually
# 3. PR is merged to master

Expand All @@ -19,7 +19,7 @@ env:
jobs:
interaction-tests:
name: Run Interaction Tests
runs-on: ubuntu-latest # nosemgrep: non-self-hosted-runner
runs-on: ubuntu-22.04 # nosemgrep: non-self-hosted-runner
if: ${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'Run Interaction Tests' || github.event_name == 'push' }}
steps:
- name: Checkout Codebase
Expand All @@ -33,5 +33,5 @@ jobs:
- name: Run Interaction Tests
run: |
npx playwright install chromium firefox webkit --with-deps
yarn test:react:interaction:ci
yarn test:react:interaction:ci
working-directory: packages/blade
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,11 @@ const useFilteredItems = (
} => {
const childrenArray = React.Children.toArray(children); // Convert children to an array

const { filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer } = useDropdown();
const { filteredValues, hasAutoCompleteInHeader, dropdownTriggerer } = useDropdown();

const items = React.useMemo(() => {
const hasAutoComplete =
hasAutoCompleteInBottomSheetHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
hasAutoCompleteInHeader || dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;

if (!hasAutoComplete) {
return childrenArray;
Expand All @@ -98,7 +97,7 @@ const useFilteredItems = (
// @ts-expect-error: props does exist
const filteredItems = childrenArray.filter((item) => filteredValues.includes(item.props.value));
return filteredItems;
}, [filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer, childrenArray]);
}, [filteredValues, hasAutoCompleteInHeader, dropdownTriggerer, childrenArray]);

return {
itemData: items,
Expand Down
10 changes: 4 additions & 6 deletions packages/blade/src/components/ActionList/ActionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,9 @@ const _ActionListSection = ({
_sectionChildValues,
...rest
}: ActionListSectionProps): React.ReactElement => {
const { hasAutoCompleteInBottomSheetHeader, dropdownTriggerer, filteredValues } = useDropdown();
const { hasAutoCompleteInHeader, dropdownTriggerer, filteredValues } = useDropdown();
const hasAutoComplete =
hasAutoCompleteInBottomSheetHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
hasAutoCompleteInHeader || dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;

const isSectionVisible = React.useMemo(() => {
if (hasAutoComplete) {
Expand Down Expand Up @@ -296,12 +295,11 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => {
dropdownTriggerer,
isKeydownPressed,
filteredValues,
hasAutoCompleteInBottomSheetHeader,
hasAutoCompleteInHeader,
} = useDropdown();

const hasAutoComplete =
hasAutoCompleteInBottomSheetHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
hasAutoCompleteInHeader || dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;

const renderOnWebAs = props.href ? 'a' : 'button';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const _BottomSheet = ({

// if bottomSheet height is >35% & <50% then set initial snapPoint to 35%
useIsomorphicLayoutEffect(() => {
if (bottomSheetAndDropdownGlue?.hasAutoCompleteInBottomSheetHeader) {
if (bottomSheetAndDropdownGlue?.hasAutoCompleteInHeader) {
// In AutoComplete, we want to open BottomSheet with max height so we set this to last index
initialSnapPoint.current = 2;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ const _BottomSheet = ({
const setPositionY = React.useCallback(
(value: number, limit = true) => {
// In AutoComplete, we want BottomSheet to be docked to top snappoint so we remove the limits
const shouldLimitPositionY =
limit && !bottomSheetAndDropdownGlue?.hasAutoCompleteInBottomSheetHeader;
const shouldLimitPositionY = limit && !bottomSheetAndDropdownGlue?.hasAutoCompleteInHeader;

const maxValue = computeMaxContent({
contentHeight,
Expand All @@ -129,7 +128,7 @@ const _BottomSheet = ({
_setPositionY(shouldLimitPositionY ? maxValue : value);
},
[
bottomSheetAndDropdownGlue?.hasAutoCompleteInBottomSheetHeader,
bottomSheetAndDropdownGlue?.hasAutoCompleteInHeader,
contentHeight,
footerHeight,
grabHandleHeight,
Expand Down Expand Up @@ -164,7 +163,7 @@ const _BottomSheet = ({

// if bottomSheet height is >35% & <50% then set initial snapPoint to 35%
useIsomorphicLayoutEffect(() => {
if (bottomSheetAndDropdownGlue?.hasAutoCompleteInBottomSheetHeader) {
if (bottomSheetAndDropdownGlue?.hasAutoCompleteInHeader) {
initialSnapPoint.current = AUTOCOMPLETE_DEFAULT_SNAPPOINT;
} else {
const middleSnapPoint = snapPoints[1] * dimensions.height;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const useBottomSheetContext = (): BottomSheetContextProps => {

type BottomSheetAndDropdownGlueContext = {
isOpen: boolean;
hasAutoCompleteInBottomSheetHeader: boolean;
hasAutoCompleteInHeader: boolean;
/**
* This flag is true when <Dropdown> contains or renders <BottomSheet> inside of it
* We can use this flag to alter behavior or styles of Dropdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ const _BottomSheetHeader = ({
// we don't set focus on close button when it has AutoComplete inside.
// We set focus on AutoComplete instead inside AutoComplete component
closeButtonRef={
bottomSheetAndDropdownGlue?.hasAutoCompleteInBottomSheetHeader
? undefined
: defaultInitialFocusRef
bottomSheetAndDropdownGlue?.hasAutoCompleteInHeader ? undefined : defaultInitialFocusRef
}
// back button
showBackButton={showBackButton}
Expand Down
15 changes: 7 additions & 8 deletions packages/blade/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,7 @@ const _Dropdown = (
const [activeTagIndex, setActiveTagIndex] = React.useState(-1);
const [shouldIgnoreBlurAnimation, setShouldIgnoreBlurAnimation] = React.useState(false);
const [hasFooterAction, setHasFooterAction] = React.useState(false);
const [
hasAutoCompleteInBottomSheetHeader,
setHasAutoCompleteInBottomSheetHeader,
] = React.useState(false);
const [hasAutoCompleteInHeader, setHasAutoCompleteInHeader] = React.useState(false);
const [isKeydownPressed, setIsKeydownPressed] = React.useState(false);
const [changeCallbackTriggerer, setChangeCallbackTriggerer] = React.useState<
DropdownContextType['changeCallbackTriggerer']
Expand All @@ -98,6 +95,7 @@ const _Dropdown = (
* */
const triggererWrapperRef = React.useRef<ContainerElementType>(null);
const triggererRef = React.useRef<HTMLButtonElement>(null);
const headerAutoCompleteRef = React.useRef<HTMLButtonElement>(null);
const actionListItemRef = React.useRef<HTMLDivElement>(null);
const dropdownTriggerer = React.useRef<DropdownContextType['dropdownTriggerer']>();
const isTagDismissedRef = React.useRef<{ value: boolean } | null>({ value: false });
Expand Down Expand Up @@ -188,13 +186,14 @@ const _Dropdown = (
setIsKeydownPressed,
dropdownBaseId,
triggererRef,
headerAutoCompleteRef,
triggererWrapperRef,
actionListItemRef,
selectionType,
hasFooterAction,
setHasFooterAction,
hasAutoCompleteInBottomSheetHeader,
setHasAutoCompleteInBottomSheetHeader,
hasAutoCompleteInHeader,
setHasAutoCompleteInHeader,
dropdownTriggerer: dropdownTriggerer.current,
changeCallbackTriggerer,
setChangeCallbackTriggerer,
Expand Down Expand Up @@ -225,13 +224,13 @@ const _Dropdown = (
return {
isOpen: isDropdownOpen,
dropdownHasBottomSheet,
hasAutoCompleteInBottomSheetHeader,
hasAutoCompleteInHeader,
setDropdownHasBottomSheet,
// This is the dismiss function which will be injected into the BottomSheet
// Basically <BottomSheet onDismiss={onBottomSheetDismiss} />
onBottomSheetDismiss: close,
};
}, [dropdownHasBottomSheet, hasAutoCompleteInBottomSheetHeader, isDropdownOpen, close]);
}, [dropdownHasBottomSheet, hasAutoCompleteInHeader, isDropdownOpen, close]);

return (
<BottomSheetAndDropdownGlueContext.Provider value={BottomSheetAndDropdownGlueContextValue}>
Expand Down
16 changes: 13 additions & 3 deletions packages/blade/src/components/Dropdown/DropdownHeaderFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type DropdownHeaderProps = Pick<
| 'trailing'
| 'titleSuffix'
| 'testID'
| 'children'
| keyof DataAnalyticsAttribute
>;

Expand All @@ -31,8 +32,11 @@ const _DropdownHeader = ({
titleSuffix,
trailing,
testID,
children,
...rest
}: DropdownHeaderProps): React.ReactElement => {
const { hasAutoCompleteInHeader, setShouldIgnoreBlurAnimation } = useDropdown();

return (
<BaseBox
flexShrink={0}
Expand All @@ -41,8 +45,12 @@ const _DropdownHeader = ({
: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onMouseDown: (e: any) => {
// we don't want focus to ever move on header because its static element
e.preventDefault();
// we don't want focus to ever move on header because its static element except when autocomplete is present
if (!hasAutoCompleteInHeader) {
e.preventDefault();
} else {
setShouldIgnoreBlurAnimation(false);
}
},
})}
>
Expand All @@ -59,7 +67,9 @@ const _DropdownHeader = ({
// close button
showCloseButton={false}
{...makeAnalyticsAttribute(rest)}
/>
>
{children}
</BaseHeader>
</BaseBox>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Checkbox } from '~components/Checkbox';
import { Button } from '~components/Button';
import { Badge } from '~components/Badge';
import { Amount } from '~components/Amount';
import { AutoComplete } from '~components/Input/DropdownInputTriggers';
import { Tooltip, TooltipInteractiveWrapper } from '~components/Tooltip';

const DropdownStoryMeta = {
Expand Down Expand Up @@ -233,6 +234,34 @@ export const InternalAutoPositioning = (): React.ReactElement => {
);
};

const items = ['Apples', 'Appricots', 'Cherries', 'Crab apples', 'Jambolan'];

export const InternalDropdownWithSearch = (): React.ReactElement => {
const [selected, setSelected] = React.useState<string[]>([]);

return (
<Dropdown selectionType="multiple" margin="spacing.4">
<DropdownButton variant="tertiary">Fruits: {selected.length}</DropdownButton>
<DropdownOverlay width="500px" maxWidth="500px">
<DropdownHeader>
<AutoComplete label="Search Fruits" />
</DropdownHeader>
<ActionList>
{items.map((item) => (
<ActionListItem
key={item}
title={item}
value={item}
onClick={() => setSelected(Array.from(new Set([...selected, item])))}
isSelected={selected.includes(item)}
/>
))}
</ActionList>
</DropdownOverlay>
</Dropdown>
);
};

export const InternalLinkDropdown = (): React.ReactElement => {
const [status, setStatus] = React.useState<string | undefined>('latest-added');
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import { HomeIcon } from '~components/Icons';
import { Button } from '~components/Button';
import { Box } from '~components/Box';
import { SearchInput } from '~components/Input/SearchInput';

Check failure on line 30 in packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx

View workflow job for this annotation

GitHub Actions / Validate Source Code

'SearchInput' is defined but never used. Allowed unused vars must match /^ignored/u

const DropdownStoryMeta: Meta = {
title: 'Components/Dropdown/With Select',
Expand Down Expand Up @@ -409,6 +410,26 @@
},
};

export const InternalDropdownWithSearch = (): React.ReactElement => {
return (
<Dropdown selectionType="multiple">
<SelectInput label="Select fruits" />
<DropdownOverlay>
<DropdownHeader>
<AutoComplete label="Search Fruits" />
</DropdownHeader>
<ActionList>
<ActionListItem title="Apples" value="Apples" />
<ActionListItem title="Appricots" value="Appricots" />
<ActionListItem title="Cherries" value="Cherries" />
<ActionListItem title="Crab apples" value="Crab apples" />
<ActionListItem title="Jambolan" value="Jambolan" />
</ActionList>
</DropdownOverlay>
</Dropdown>
);
};

export const InternalDropdownPerformance = (): React.ReactElement => {
const fruits = [
'Apples',
Expand Down
25 changes: 18 additions & 7 deletions packages/blade/src/components/Dropdown/useDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type DropdownContextType = {
| 'SearchInput';
/** ref of triggerer. Used to call focus in certain places */
triggererRef: React.MutableRefObject<HTMLButtonElement | null>;
headerAutoCompleteRef: React.MutableRefObject<HTMLButtonElement | null>;
triggererWrapperRef: React.MutableRefObject<ContainerElementType | null>;
actionListItemRef: React.RefObject<HTMLDivElement | null>;
isTagDismissedRef: React.RefObject<{ value: boolean } | null>;
Expand All @@ -98,8 +99,8 @@ type DropdownContextType = {
/**
* Apart from dropdownTriggerer prop, we also set this boolean because in BottomSheet, the initial trigger can be Select but also have autocomplete inside of it
*/
hasAutoCompleteInBottomSheetHeader: boolean;
setHasAutoCompleteInBottomSheetHeader: (value: boolean) => void;
hasAutoCompleteInHeader: boolean;
setHasAutoCompleteInHeader: (value: boolean) => void;

/**
* A value that can be used in dependency array to know when Dropdown value is changed.
Expand Down Expand Up @@ -141,8 +142,8 @@ const DropdownContext = React.createContext<DropdownContextType>({
setShouldIgnoreBlurAnimation: noop,
hasFooterAction: false,
setHasFooterAction: noop,
hasAutoCompleteInBottomSheetHeader: false,
setHasAutoCompleteInBottomSheetHeader: noop,
hasAutoCompleteInHeader: false,
setHasAutoCompleteInHeader: noop,
isKeydownPressed: false,
setIsKeydownPressed: noop,
changeCallbackTriggerer: 0,
Expand All @@ -156,6 +157,9 @@ const DropdownContext = React.createContext<DropdownContextType>({
triggererRef: {
current: null,
},
headerAutoCompleteRef: {
current: null,
},
isTagDismissedRef: {
current: null,
},
Expand Down Expand Up @@ -329,7 +333,7 @@ const useDropdown = (): UseDropdownReturnValue => {
const newIndex = index ?? activeIndex;
let updatedIndex: number;
const hasAutoComplete =
rest.hasAutoCompleteInBottomSheetHeader ||
rest.hasAutoCompleteInHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
if (hasAutoComplete && filteredValues.length > 0) {
// When its autocomplete, we don't loop over all options. We only loop on filtered options
Expand Down Expand Up @@ -374,13 +378,20 @@ const useDropdown = (): UseDropdownReturnValue => {
e: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLInputElement>,
index: number,
): void => {
setIsKeydownPressed(false);
const actionType = getActionFromKey(e, isOpen, dropdownTriggerer);
if (typeof actionType === 'number') {
onOptionChange(actionType, index);
}
selectOption(index);

if (!isReactNative()) {
rest.triggererRef.current?.focus();
if (rest.hasAutoCompleteInHeader) {
// move focus to autocomplete
rest.headerAutoCompleteRef.current?.focus();
} else {
rest.triggererRef.current?.focus();
}
}
};

Expand All @@ -396,7 +407,7 @@ const useDropdown = (): UseDropdownReturnValue => {
setIsOpen(true);

if (
rest.hasAutoCompleteInBottomSheetHeader ||
rest.hasAutoCompleteInHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete
) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ const _StyledBaseInput: React.ForwardRefRenderFunction<
{...commonProps}
{...props}
{...accessibilityProps}
tabIndex={0}
/>
);
};
Expand Down
Loading
Loading