Skip to content

Commit deff808

Browse files
committed
chore: sync with master
2 parents cf32230 + c045a7b commit deff808

File tree

10 files changed

+212
-53
lines changed

10 files changed

+212
-53
lines changed

packages/blade/src/components/ActionList/ActionListItem.tsx

+46-22
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,39 @@ const ActionListItemText = assignWithoutSideEffects(_ActionListItemText, {
251251
componentId: componentIds.ActionListItemText,
252252
});
253253

254+
const BaseMenuLeadingItem = ({
255+
isSelected,
256+
leading,
257+
selectionType,
258+
isDisabled,
259+
}: {
260+
isSelected?: boolean;
261+
leading?: React.ReactNode;
262+
selectionType: string | undefined;
263+
isDisabled?: boolean;
264+
}): React.ReactElement | null => {
265+
if (selectionType === 'multiple') {
266+
return (
267+
<BaseBox
268+
pointerEvents="none"
269+
// 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
270+
{...makeAccessible({
271+
hidden: true,
272+
})}
273+
>
274+
<Checkbox isChecked={isSelected} tabIndex={-1} isDisabled={isDisabled}>
275+
{/*
276+
Checkbox requires children. Didn't want to make it optional because its helpful for consumers
277+
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
278+
*/}
279+
{null}
280+
</Checkbox>
281+
</BaseBox>
282+
);
283+
}
284+
return React.isValidElement(leading) ? leading : null;
285+
};
286+
254287
type ClickHandlerType = (e: React.MouseEvent<HTMLButtonElement>) => void;
255288

256289
const makeActionListItemClickable = (
@@ -297,26 +330,29 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => {
297330
isKeydownPressed,
298331
filteredValues,
299332
hasAutoCompleteInBottomSheetHeader,
333+
hasUnControlledFilterChipSelectInput,
300334
} = useDropdown();
301335

302336
const hasAutoComplete =
303337
hasAutoCompleteInBottomSheetHeader ||
304338
dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete;
305339

306340
const renderOnWebAs = props.href ? 'a' : 'button';
307-
308341
/**
309342
* In SelectInput, returns the isSelected according to selected indexes in the state
310343
*
311344
* In Other Triggers (Menu Usecase), returns `props.isSelected` since passing the
312345
* isSelected prop explicitly is the only way to select item in menu
313346
*/
314347
const getIsSelected = (): boolean | undefined => {
315-
if (dropdownTriggerer === dropdownComponentIds.triggers.SelectInput || hasAutoComplete) {
348+
if (
349+
dropdownTriggerer === dropdownComponentIds.triggers.SelectInput ||
350+
hasAutoComplete ||
351+
hasUnControlledFilterChipSelectInput
352+
) {
316353
if (typeof props._index === 'number') {
317354
return selectedIndices.includes(props._index);
318355
}
319-
320356
return undefined;
321357
}
322358

@@ -359,25 +395,13 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => {
359395
title={props.title}
360396
description={props.description}
361397
leading={
362-
selectionType === 'multiple' ? (
363-
<BaseBox
364-
pointerEvents="none"
365-
// 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
366-
{...makeAccessible({
367-
hidden: true,
368-
})}
369-
>
370-
<Checkbox isChecked={isSelected} tabIndex={-1} isDisabled={props.isDisabled}>
371-
{/*
372-
Checkbox requires children. Didn't want to make it optional because its helpful for consumers
373-
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
374-
*/}
375-
{null}
376-
</Checkbox>
377-
</BaseBox>
378-
) : (
379-
props.leading
380-
)
398+
<BaseMenuLeadingItem
399+
key={`${dropdownBaseId}-${props._index}-leading-${isSelected}`}
400+
isSelected={isSelected}
401+
leading={props.leading}
402+
selectionType={selectionType}
403+
isDisabled={props.isDisabled}
404+
/>
381405
}
382406
trailing={props.trailing}
383407
titleSuffix={props.titleSuffix}

packages/blade/src/components/BottomSheet/__tests__/__snapshots__/BottomSheet.web.test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ exports[`<BottomSheet /> should compose with DropdownButton 1`] = `
142142
-ms-flex-direction: column;
143143
flex-direction: column;
144144
padding-right: 8px;
145-
padding-left: 0px;
145+
padding-left: 8px;
146146
}
147147
148148
.c22.c22.c22.c22.c22 {

packages/blade/src/components/Dropdown/Dropdown.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ const _Dropdown = (
8585
hasAutoCompleteInBottomSheetHeader,
8686
setHasAutoCompleteInBottomSheetHeader,
8787
] = React.useState(false);
88+
const [
89+
hasUnControlledFilterChipSelectInput,
90+
setHasUnControlledFilterChipSelectInput,
91+
] = React.useState(false);
8892
const [isKeydownPressed, setIsKeydownPressed] = React.useState(false);
8993
const [changeCallbackTriggerer, setChangeCallbackTriggerer] = React.useState<
9094
DropdownContextType['changeCallbackTriggerer']
@@ -199,6 +203,8 @@ const _Dropdown = (
199203
setHasFooterAction,
200204
hasAutoCompleteInBottomSheetHeader,
201205
setHasAutoCompleteInBottomSheetHeader,
206+
hasUnControlledFilterChipSelectInput,
207+
setHasUnControlledFilterChipSelectInput,
202208
dropdownTriggerer: dropdownTriggerer.current,
203209
changeCallbackTriggerer,
204210
setChangeCallbackTriggerer,

packages/blade/src/components/Dropdown/FilterChipSelectInput.web.tsx

+92-24
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
33

4-
import React from 'react';
4+
import React, { useEffect } from 'react';
55
import { useDropdown } from './useDropdown';
66
import { dropdownComponentIds } from './dropdownComponentIds';
77
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
88
import { BaseFilterChip } from '~components/FilterChip/BaseFilterChip';
99
import { getActionListContainerRole } from '~components/ActionList/getA11yRoles';
1010
import type { BaseFilterChipProps } from '~components/FilterChip/types';
1111
import type { DataAnalyticsAttribute } from '~utils/types';
12+
import { useId } from '~utils/useId';
13+
import { useFirstRender } from '~utils/useFirstRender';
1214

13-
type FilterChipSelectInput = Pick<
15+
type FilterChipSelectInputProps = Pick<
1416
BaseFilterChipProps,
15-
| 'onKeyDown'
16-
| 'value'
17-
| 'onClearButtonClick'
18-
| 'label'
19-
| 'testID'
20-
| 'onClick'
21-
| 'selectionType'
22-
| 'onBlur'
17+
'onKeyDown' | 'value' | 'label' | 'testID' | 'onClick' | 'selectionType' | 'onBlur'
2318
> & {
2419
accessibilityLabel?: string;
20+
onChange?: (props: { name: string; values: string[] }) => void;
21+
name?: string;
22+
onClearButtonClick?: (props: { name: string; values: string[] }) => void;
2523
} & DataAnalyticsAttribute;
2624

27-
const _FilterChipSelectInput = ({
28-
onClick,
29-
onBlur,
30-
onKeyDown,
31-
accessibilityLabel,
32-
testID,
33-
value,
34-
onClearButtonClick,
35-
label,
36-
...rest
37-
}: FilterChipSelectInput): React.ReactElement => {
25+
const _FilterChipSelectInput = (props: FilterChipSelectInputProps): React.ReactElement => {
26+
const idBase = useId('filter-chip-select-input');
3827
const {
28+
onClick,
29+
onBlur,
30+
onKeyDown,
31+
accessibilityLabel,
32+
testID,
33+
value,
34+
onClearButtonClick,
35+
label,
36+
onChange,
37+
name,
38+
...rest
39+
} = props;
40+
const [uncontrolledInputValue, setUncontrolledInputValue] = React.useState<string[]>([]);
41+
const isFirstRender = useFirstRender();
42+
43+
const {
44+
options,
45+
selectedIndices,
3946
onTriggerClick,
4047
onTriggerKeydown,
4148
dropdownBaseId,
@@ -44,19 +51,80 @@ const _FilterChipSelectInput = ({
4451
hasFooterAction,
4552
triggererRef,
4653
selectionType,
54+
isControlled,
55+
setHasUnControlledFilterChipSelectInput,
56+
setSelectedIndices,
57+
controlledValueIndices,
58+
changeCallbackTriggerer,
4759
} = useDropdown();
4860

61+
const isUnControlled = options.length > 0 && props.value === undefined;
62+
63+
useEffect(() => {
64+
if (isUnControlled) {
65+
setHasUnControlledFilterChipSelectInput(true);
66+
}
67+
// eslint-disable-next-line react-hooks/exhaustive-deps
68+
}, [isUnControlled]);
69+
70+
const getValuesArrayFromIndices = (): string[] => {
71+
let indices: number[] = [];
72+
if (isControlled) {
73+
indices = controlledValueIndices;
74+
} else {
75+
indices = selectedIndices;
76+
}
77+
78+
return indices.map((selectionIndex) => options[selectionIndex].value);
79+
};
80+
81+
const getTitleFromValue = (value: string): string => {
82+
const option = options.find((option) => option.value === value);
83+
return option ? option.title : '';
84+
};
85+
86+
const getUnControlledFilterChipValue = (): string | string[] => {
87+
if (selectionType === 'single') {
88+
if (uncontrolledInputValue.length > 0) {
89+
return getTitleFromValue(uncontrolledInputValue[0]);
90+
}
91+
return '';
92+
}
93+
return uncontrolledInputValue;
94+
};
95+
96+
const handleClearButtonClick = (): void => {
97+
props.onClearButtonClick?.({ name: name ?? idBase, values: getValuesArrayFromIndices() });
98+
if (isUnControlled) {
99+
setUncontrolledInputValue([]);
100+
setSelectedIndices([]);
101+
}
102+
};
103+
104+
useEffect(() => {
105+
if (!isFirstRender) {
106+
props.onChange?.({
107+
name: props.name || idBase,
108+
values: getValuesArrayFromIndices(),
109+
});
110+
if (isUnControlled) {
111+
setUncontrolledInputValue(getValuesArrayFromIndices());
112+
}
113+
}
114+
// eslint-disable-next-line react-hooks/exhaustive-deps
115+
}, [changeCallbackTriggerer]);
116+
49117
return (
50118
<BaseFilterChip
51119
label={label}
52-
value={value}
53-
onClearButtonClick={onClearButtonClick}
120+
value={value ?? getUnControlledFilterChipValue()}
121+
onClearButtonClick={handleClearButtonClick}
54122
selectionType={selectionType}
55123
{...rest}
56124
ref={triggererRef as any}
57125
accessibilityProps={{
58126
label: accessibilityLabel ?? label,
59-
hasPopup: getActionListContainerRole(hasFooterAction, 'DropdownButton'),
127+
hasPopup: getActionListContainerRole(hasFooterAction, 'FilterChipSelectInput'),
60128
expanded: isOpen,
61129
controls: `${dropdownBaseId}-actionlist`,
62130
activeDescendant: activeIndex >= 0 ? `${dropdownBaseId}-${activeIndex}` : undefined,

packages/blade/src/components/Dropdown/__tests__/__snapshots__/Dropdown.native.test.tsx.snap

+2-2
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,7 @@ exports[`<Dropdown /> should render dropdown 1`] = `
11841184
{
11851185
"display": "flex",
11861186
"flexDirection": "column",
1187-
"paddingLeft": 0,
1187+
"paddingLeft": 8,
11881188
"paddingRight": 8,
11891189
},
11901190
]
@@ -1363,7 +1363,7 @@ exports[`<Dropdown /> should render dropdown 1`] = `
13631363
{
13641364
"display": "flex",
13651365
"flexDirection": "column",
1366-
"paddingLeft": 0,
1366+
"paddingLeft": 8,
13671367
"paddingRight": 8,
13681368
},
13691369
]

packages/blade/src/components/Dropdown/__tests__/__snapshots__/Dropdown.web.test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -2178,7 +2178,7 @@ exports[`<Dropdown /> with <FilterChipSelectInput/> should support data-analytic
21782178
data-blade-component="base-box"
21792179
>
21802180
<button
2181-
aria-controls="dropdown-503-actionlist"
2181+
aria-controls="dropdown-514-actionlist"
21822182
aria-expanded="false"
21832183
aria-haspopup="menu"
21842184
aria-label="profile"

packages/blade/src/components/Dropdown/docs/DropdownWithFilterChip.stories.tsx

+51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { DropdownButton } from '../DropdownButton';
33
import { Dropdown, DropdownOverlay } from '..';
44
import { FilterChipSelectInput } from '../FilterChipSelectInput';
55
import { ActionList, ActionListItem } from '~components/ActionList';
6+
import { Box } from '~components/Box';
7+
import { Text } from '~components/Typography';
68

79
const DropdownStoryMeta = {
810
title: 'Components/Dropdown/With Filter Chip',
@@ -124,4 +126,53 @@ export const SelectionTypeMultiple = (): React.ReactElement => {
124126
);
125127
};
126128

129+
export const UncontrolledFilterChipSelectInput = (): React.ReactElement => {
130+
return (
131+
<Box>
132+
<Text size="small" weight="semibold" color="interactive.text.primary.normal">
133+
Uncontrolled Filter Chip Select Input - Single
134+
</Text>
135+
<Dropdown selectionType="single">
136+
<FilterChipSelectInput
137+
label="Filter Chip"
138+
onChange={(value) => {
139+
console.log('value', value);
140+
}}
141+
onClearButtonClick={(value) => {
142+
console.log('value', value);
143+
}}
144+
/>
145+
<DropdownOverlay>
146+
<ActionList>
147+
<ActionListItem title="Latest Added" value="latest-added" />
148+
<ActionListItem title="Latest Invoice" value="latest-invoice" />
149+
<ActionListItem title="Oldest Due Date" value="oldest-due-date" />
150+
</ActionList>
151+
</DropdownOverlay>
152+
</Dropdown>
153+
<Text size="small" weight="semibold" color="interactive.text.primary.normal">
154+
Uncontrolled Filter Chip Select Input - Multiple
155+
</Text>
156+
<Dropdown selectionType="multiple">
157+
<FilterChipSelectInput
158+
label="Filter Chip"
159+
onChange={(value) => {
160+
console.log('value', value);
161+
}}
162+
onClearButtonClick={(value) => {
163+
console.log('value', value);
164+
}}
165+
/>
166+
<DropdownOverlay>
167+
<ActionList>
168+
<ActionListItem title="Latest Added" value="latest-added" />
169+
<ActionListItem title="Latest Invoice" value="latest-invoice" />
170+
<ActionListItem title="Oldest Due Date" value="oldest-due-date" />
171+
</ActionList>
172+
</DropdownOverlay>
173+
</Dropdown>
174+
</Box>
175+
);
176+
};
177+
127178
export default DropdownStoryMeta;

packages/blade/src/components/Dropdown/useDropdown.ts

+7
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ type DropdownContextType = {
9797
hasFooterAction: boolean;
9898
setHasFooterAction: (value: boolean) => void;
9999

100+
/**
101+
* Whether the FilterChipSelectInput is uncontrolled
102+
*/
103+
hasUnControlledFilterChipSelectInput: boolean;
104+
setHasUnControlledFilterChipSelectInput: (value: boolean) => void;
100105
/**
101106
* 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
102107
*/
@@ -151,6 +156,8 @@ const DropdownContext = React.createContext<DropdownContextType>({
151156
setChangeCallbackTriggerer: noop,
152157
isControlled: false,
153158
setIsControlled: noop,
159+
hasUnControlledFilterChipSelectInput: false,
160+
setHasUnControlledFilterChipSelectInput: noop,
154161
dropdownBaseId: '',
155162
actionListItemRef: {
156163
current: null,

0 commit comments

Comments
 (0)