Skip to content

Commit 1646e97

Browse files
authored
fix(FilterPicker): unescape symbols (#745)
1 parent 0a560e6 commit 1646e97

File tree

4 files changed

+118
-11
lines changed

4 files changed

+118
-11
lines changed

.changeset/funny-chicken-stare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": patch
3+
---
4+
5+
Unescape keys in FilterPicker to support `:` and `=` symbols.

src/components/fields/DatePicker/DateRangePicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { useFocusManagerRef } from './utils';
3737
const DateRangeDash = tasty({
3838
'aria-hidden': 'true',
3939
'data-qa': 'DateRangeDash',
40-
children: '–',
40+
children: '–',
4141
styles: {
4242
padding: '0 .5x',
4343
},

src/components/fields/FilterPicker/FilterPicker.stories.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,3 +1614,80 @@ export const WithSelectAll: Story = {
16141614
},
16151615
},
16161616
};
1617+
1618+
export const MultipleControlled: Story = {
1619+
args: {
1620+
label: 'Controlled Multiple Selection',
1621+
placeholder: 'Choose permissions...',
1622+
selectionMode: 'multiple',
1623+
isCheckable: true,
1624+
searchPlaceholder: 'Filter permissions...',
1625+
width: 'max 30x',
1626+
},
1627+
play: async ({ canvasElement }) => {},
1628+
render: (args) => {
1629+
const [selectedKeys, setSelectedKeys] = useState<string[]>([
1630+
'read',
1631+
'write',
1632+
]);
1633+
1634+
return (
1635+
<Space gap="2x" flow="column" placeItems="start">
1636+
<FilterPicker
1637+
{...args}
1638+
selectedKeys={selectedKeys}
1639+
onSelectionChange={(keys) => setSelectedKeys(keys as string[])}
1640+
>
1641+
{permissions.map((permission) => (
1642+
<FilterPicker.Item
1643+
key={permission.key}
1644+
textValue={permission.label}
1645+
description={permission.description}
1646+
>
1647+
{permission.label}
1648+
</FilterPicker.Item>
1649+
))}
1650+
</FilterPicker>
1651+
1652+
<Text>
1653+
Selected:{' '}
1654+
<strong>
1655+
{selectedKeys.length ? selectedKeys.join(', ') : 'None'}
1656+
</strong>
1657+
</Text>
1658+
1659+
<Space gap="1x" flow="row">
1660+
<Button
1661+
size="small"
1662+
type="outline"
1663+
onClick={() => setSelectedKeys(['read', 'write', 'admin'])}
1664+
>
1665+
Select Admin Set
1666+
</Button>
1667+
<Button
1668+
size="small"
1669+
type="outline"
1670+
onClick={() => setSelectedKeys(['read'])}
1671+
>
1672+
Read Only
1673+
</Button>
1674+
<Button
1675+
size="small"
1676+
type="outline"
1677+
onClick={() => setSelectedKeys([])}
1678+
>
1679+
Clear All
1680+
</Button>
1681+
</Space>
1682+
</Space>
1683+
);
1684+
},
1685+
parameters: {
1686+
docs: {
1687+
description: {
1688+
story:
1689+
'A controlled FilterPicker in multiple selection mode where the selection state is managed externally. The component responds to external state changes and provides selection feedback through the onSelectionChange callback. This pattern is useful when you need to programmatically control the selection or integrate with form libraries.',
1690+
},
1691+
},
1692+
},
1693+
};

src/components/fields/FilterPicker/FilterPicker.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { FocusScope, Key, useKeyboard } from 'react-aria';
1717
import { Section as BaseSection, Item, ListState } from 'react-stately';
1818

1919
import { useWarn } from '../../../_internal/hooks/use-warn';
20-
import { DirectionIcon } from '../../../icons';
20+
import { DirectionIcon, LoadingIcon } from '../../../icons';
2121
import { useProviderProps } from '../../../provider';
2222
import {
2323
BASE_STYLES,
@@ -238,6 +238,21 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
238238
const cachedItemsOrder = useRef<T[] | null>(null);
239239
const triggerRef = useRef<HTMLButtonElement>(null);
240240

241+
// ---------------------------------------------------------------------------
242+
// Invalidate cached sorting whenever the available options change.
243+
// This ensures newly provided options are displayed and properly sorted on
244+
// the next popover open instead of re-using a stale order from a previous
245+
// session (which caused only the previously selected options to be rendered
246+
// or the list to appear unsorted).
247+
// ---------------------------------------------------------------------------
248+
useEffect(() => {
249+
cachedChildrenOrder.current = null;
250+
}, [children]);
251+
252+
useEffect(() => {
253+
cachedItemsOrder.current = null;
254+
}, [items]);
255+
241256
const isControlledSingle = selectedKey !== undefined;
242257
const isControlledMultiple = selectedKeys !== undefined;
243258

@@ -252,12 +267,20 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
252267
// can compare them with user-provided keys.
253268
const normalizeKeyValue = (key: Key): string => {
254269
if (key == null) return '';
255-
const str = String(key);
256-
return str.startsWith('.$')
257-
? str.slice(2)
258-
: str.startsWith('.')
259-
? str.slice(1)
260-
: str;
270+
// React escapes "=" as "=0" and ":" as "=2" when it stores keys internally.
271+
// We strip the possible React prefixes first and then un-escape those sequences
272+
// so that callers work with the original key values supplied by the user.
273+
let str = String(key);
274+
275+
// Remove React array/object key prefixes (".$" or ".") if present.
276+
if (str.startsWith('.$')) {
277+
str = str.slice(2);
278+
} else if (str.startsWith('.')) {
279+
str = str.slice(1);
280+
}
281+
282+
// Un-escape React's internal key encodings.
283+
return str.replace(/=2/g, ':').replace(/=0/g, '=');
261284
};
262285

263286
// ---------------------------------------------------------------------------
@@ -867,16 +890,17 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
867890
type={type}
868891
theme={validationState === 'invalid' ? 'danger' : theme}
869892
size={size}
870-
isDisabled={isDisabled}
871-
isLoading={isLoading}
893+
isDisabled={isDisabled || isLoading}
872894
mods={{
873895
placeholder: !hasSelection,
874896
selected: hasSelection,
875897
...externalMods,
876898
}}
877899
icon={icon}
878900
rightIcon={
879-
rightIcon !== undefined ? (
901+
isLoading ? (
902+
<LoadingIcon />
903+
) : rightIcon !== undefined ? (
880904
rightIcon
881905
) : (
882906
<DirectionIcon to={state.isOpen ? 'top' : 'bottom'} />
@@ -927,6 +951,7 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
927951
selectionMode={selectionMode}
928952
validationState={validationState}
929953
isDisabled={isDisabled}
954+
isLoading={isLoading}
930955
stateRef={listStateRef}
931956
isCheckable={isCheckable}
932957
mods={{

0 commit comments

Comments
 (0)