Skip to content

Commit

Permalink
feat(Menu): Integration Branch (#2244)
Browse files Browse the repository at this point in the history
  • Loading branch information
saurabhdaware authored Jun 25, 2024
1 parent 378831b commit 2cb7668
Show file tree
Hide file tree
Showing 62 changed files with 3,884 additions and 711 deletions.
5 changes: 5 additions & 0 deletions .changeset/yellow-planes-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@razorpay/blade": minor
---

feat(Menu): add Menu component
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,12 @@ const ActionListWithCustomItemsExample: StoryFn<typeof ActionListComponent> = ()
value="profile"
leading={<ActionListItemIcon icon={UserIcon} />}
/>
<ActionListItem title="Credit" value="credit" description="check your credit here!" />
<ActionListItem
title="Credit"
value="credit"
leading={<ActionListItemIcon icon={UserIcon} />}
description="check your credit here!"
/>
<ActionListItem title="Disabled" value="disabled" isDisabled />
</ActionListSection>
<ActionListItem
Expand Down
221 changes: 66 additions & 155 deletions packages/blade/src/components/ActionList/ActionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
import React from 'react';
import styled from 'styled-components';
import type { TouchableOpacity } from 'react-native';
import { StyledActionListItem } from './styles/StyledActionListItem';
import { componentIds } from './componentIds';
import type { StyledActionListItemProps } from './styles/getBaseActionListItemStyles';
import { validateActionListItemProps, getNormalTextColor } from './actionListUtils';
import { getActionListItemRole, getActionListSectionRole, isRoleMenu } from './getA11yRoles';
import { getActionListItemRole, getActionListSectionRole } from './getA11yRoles';
import { Divider } from '~components/Divider';
import BaseBox from '~components/Box/BaseBox';
import type { IconComponent } from '~components/Icons';
Expand All @@ -16,11 +14,7 @@ import { Text } from '~components/Typography';
import type { Platform } from '~utils';
import { castWebType, isReactNative } from '~utils';
import { metaAttribute, MetaConstants } from '~utils/metaAttribute';
import { Checkbox } from '~components/Checkbox';
import { size } from '~tokens/global';
import type { DropdownProps } from '~components/Dropdown';
import type { StringChildrenType, TestID } from '~utils/types';
import { useTheme } from '~components/BladeProvider';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { makeSize } from '~utils/makeSize';
import { makeAccessible } from '~utils/makeAccessible';
Expand All @@ -29,6 +23,8 @@ import type { BadgeProps } from '~components/Badge';
import { Badge } from '~components/Badge';
import { Box } from '~components/Box';
import { dropdownComponentIds } from '~components/Dropdown/dropdownComponentIds';
import { BaseMenuItem, useBaseMenuItem } from '~components/BaseMenu';
import { Checkbox } from '~components/Checkbox';

type ActionListItemProps = {
title: string;
Expand Down Expand Up @@ -90,11 +86,6 @@ type ActionListItemProps = {
_index?: number;
} & TestID;

const ActionListItemContext = React.createContext<{
intent?: ActionListItemProps['intent'];
isDisabled?: ActionListItemProps['isDisabled'];
}>({});

const StyledActionListSectionTitle = styled(BaseBox)((props) => ({
// @TODO: replace this styled-component with new layout box when we have padding shorthand
padding: makeSize(props.theme.spacing[3]),
Expand Down Expand Up @@ -185,12 +176,12 @@ const ActionListSection = assignWithoutSideEffects(_ActionListSection, {

const _ActionListItemIcon = ({ icon }: { icon: IconComponent }): React.ReactElement => {
const Icon = icon;
const { intent, isDisabled } = React.useContext(ActionListItemContext);
const { color, isDisabled } = useBaseMenuItem();
const iconState = isDisabled ? 'disabled' : 'muted';
return (
<Icon
color={
intent === 'negative'
color === 'negative'
? 'feedback.icon.negative.intense'
: `interactive.icon.gray.${iconState}`
}
Expand Down Expand Up @@ -232,7 +223,7 @@ const _ActionListItemText = ({
}: {
children: StringChildrenType;
}): React.ReactElement => {
const { isDisabled } = React.useContext(ActionListItemContext);
const { isDisabled } = useBaseMenuItem();

return (
<Text variant="caption" color={getNormalTextColor(isDisabled, { isMuted: true })}>
Expand All @@ -249,7 +240,7 @@ type ClickHandlerType = (e: React.MouseEvent<HTMLButtonElement>) => void;

const makeActionListItemClickable = (
clickHandler: ClickHandlerType,
): { onPress?: StyledActionListItemProps['onPress']; onClick?: ClickHandlerType } => {
): { onPress?: (e: React.TouchEvent<TouchableOpacity>) => void; onClick?: ClickHandlerType } => {
if (isReactNative()) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
// @ts-ignore: ignoring ReactNative press type for the peace of mind
Expand All @@ -261,88 +252,6 @@ const makeActionListItemClickable = (
};
};

const _ActionListItemBody = ({
selectionType,
intent,
description,
isDisabled,
leading,
trailing,
title,
titleSuffix,
isSelected,
}: Pick<
ActionListItemProps,
'intent' | 'isDisabled' | 'description' | 'trailing' | 'leading' | 'title' | 'titleSuffix'
> & {
selectionType: DropdownProps['selectionType'];
isSelected?: boolean;
}): React.ReactElement => {
return (
<>
<BaseBox
display="flex"
justifyContent="center"
flexDirection="row"
alignItems="center"
maxHeight={isReactNative() ? undefined : makeSize(size[20])}
>
<BaseBox display="flex" justifyContent="center" alignItems="center">
{selectionType === 'multiple' ? (
// Adding aria-hidden because the listbox item in multiselect in itself explains the behaviour so announcing checkbox is unneccesary and just a nice UI tweak for us
<BaseBox
pointerEvents="none"
paddingRight="spacing.2"
{...makeAccessible({
hidden: true,
})}
>
<Checkbox isChecked={isSelected} tabIndex={-1} isDisabled={isDisabled}>
{/*
Checkbox requires children. Didn't want to make it optional because its helpful for consumers
But for this case in particular, we just want to use Text separately so that we can control spacing and color and keep it consistent with non-multiselect dropdowns
*/}
{null}
</Checkbox>
</BaseBox>
) : (
leading
)}
</BaseBox>
<BaseBox
paddingLeft={selectionType === 'multiple' || !leading ? 'spacing.0' : 'spacing.3'}
paddingRight="spacing.3"
display="flex"
alignItems="center"
flexDirection="row"
>
<Text
truncateAfterLines={1}
color={
intent === 'negative'
? 'feedback.text.negative.intense'
: getNormalTextColor(isDisabled)
}
>
{title}
</Text>
{titleSuffix}
</BaseBox>
<BaseBox marginLeft="auto">{trailing}</BaseBox>
</BaseBox>
<BaseBox paddingLeft={leading || selectionType === 'multiple' ? 'spacing.7' : undefined}>
{description ? (
<Text color={getNormalTextColor(isDisabled, { isMuted: true })} size="small">
{description}
</Text>
) : null}
</BaseBox>
</>
);
};

const ActionListItemBody = React.memo(_ActionListItemBody);

/**
* ### ActionListItem
*
Expand Down Expand Up @@ -375,8 +284,6 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => {
hasAutoCompleteInBottomSheetHeader,
} = useDropdown();

const { platform } = useTheme();
const isMobile = platform === 'onMobile';
const hasAutoComplete =
hasAutoCompleteInBottomSheetHeader ||
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
Expand Down Expand Up @@ -427,61 +334,65 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => {
}, [props.intent, dropdownTriggerer]);

return (
<ActionListItemContext.Provider value={{ intent: props.intent, isDisabled: props.isDisabled }}>
<StyledActionListItem
isVisible={hasAutoComplete && filteredValues ? filteredValues.includes(props.value) : true}
as={!isReactNative() ? renderOnWebAs : undefined}
id={`${dropdownBaseId}-${props._index}`}
type="button"
tabIndex={-1}
href={props.href}
target={props.target}
className={activeIndex === props._index ? 'active-focus' : ''}
{...makeAccessible({
selected: isSelected,
current: isRoleMenu(dropdownTriggerer) ? isSelected : undefined,
role: getActionListItemRole(dropdownTriggerer, props.href),
disabled: props.isDisabled,
})}
{...makeActionListItemClickable((e: React.MouseEvent<HTMLButtonElement>): void => {
if (typeof props._index === 'number') {
onOptionClick(e, props._index);
props.onClick?.({ name: props.value, value: isSelected, event: castWebType(e) });
}
})}
{...metaAttribute({ name: MetaConstants.ActionListItem, testID: props.testID })}
onMouseDown={() => {
// We want to keep focus on Dropdown's trigger while option is being clicked
// So We set this flag that ignores the blur animation to avoid the flicker between focus out + focus in
setShouldIgnoreBlurAnimation(true);
}}
onMouseUp={() => {
// (Contd from above comment...) We set this flag back to false since blur of SelectInput is done calling by this time
setShouldIgnoreBlurAnimation(false);
}}
data-value={props.value}
data-index={props._index}
// Custom props for changes in styles
selectionType={selectionType}
hasDescription={Boolean(props.description)}
intent={props.intent}
isSelected={isSelected}
isKeydownPressed={isKeydownPressed}
isMobile={isMobile}
>
<ActionListItemBody
selectionType={selectionType}
intent={props.intent}
description={props.description}
isDisabled={props.isDisabled}
leading={props.leading}
trailing={props.trailing}
title={props.title}
titleSuffix={props.titleSuffix}
isSelected={isSelected}
/>
</StyledActionListItem>
</ActionListItemContext.Provider>
// We use this context to change the color of subcomponents like ActionListItemIcon, ActionListItemText, etc
<BaseMenuItem
isVisible={hasAutoComplete && filteredValues ? filteredValues.includes(props.value) : true}
as={!isReactNative() ? renderOnWebAs : undefined}
id={`${dropdownBaseId}-${props._index}`}
tabIndex={-1}
title={props.title}
description={props.description}
leading={
selectionType === 'multiple' ? (
<BaseBox
pointerEvents="none"
// Adding aria-hidden because the listbox item in multiselect in itself explains the behaviour so announcing checkbox is unneccesary and just a nice UI tweak for us
{...makeAccessible({
hidden: true,
})}
>
<Checkbox isChecked={isSelected} tabIndex={-1} isDisabled={props.isDisabled}>
{/*
Checkbox requires children. Didn't want to make it optional because its helpful for consumers
But for this case in particular, we just want to use Text separately so that we can control spacing and color and keep it consistent with non-multiselect dropdowns
*/}
{null}
</Checkbox>
</BaseBox>
) : (
props.leading
)
}
trailing={props.trailing}
titleSuffix={props.titleSuffix}
href={props.href}
target={props.target}
className={activeIndex === props._index ? 'active-focus' : ''}
isSelected={isSelected}
isDisabled={props.isDisabled}
role={getActionListItemRole(dropdownTriggerer, props.href)}
{...makeActionListItemClickable((e: React.MouseEvent<HTMLButtonElement>): void => {
if (typeof props._index === 'number') {
onOptionClick(e, props._index);
props.onClick?.({ name: props.value, value: isSelected, event: castWebType(e) });
}
})}
{...metaAttribute({ name: MetaConstants.ActionListItem, testID: props.testID })}
onMouseDown={() => {
// We want to keep focus on Dropdown's trigger while option is being clicked
// So We set this flag that ignores the blur animation to avoid the flicker between focus out + focus in
setShouldIgnoreBlurAnimation(true);
}}
onMouseUp={() => {
// (Contd from above comment...) We set this flag back to false since blur of SelectInput is done calling by this time
setShouldIgnoreBlurAnimation(false);
}}
data-value={props.value}
data-index={props._index}
selectionType={selectionType}
color={props.intent}
isKeydownPressed={isKeydownPressed}
/>
);
};

Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit 2cb7668

Please sign in to comment.