diff --git a/packages/code-studio/src/styleguide/Pickers.tsx b/packages/code-studio/src/styleguide/Pickers.tsx
index 89adb62356..f6a035a6fc 100644
--- a/packages/code-studio/src/styleguide/Pickers.tsx
+++ b/packages/code-studio/src/styleguide/Pickers.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from 'react';
+import React, { cloneElement, useCallback, useState } from 'react';
import {
Flex,
Item,
@@ -6,14 +6,51 @@ import {
ItemKey,
Section,
Text,
+ PickerNormalized,
} from '@deephaven/components';
import { vsPerson } from '@deephaven/icons';
import { Icon } from '@adobe/react-spectrum';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { generateNormalizedItems, sampleSectionIdAndClasses } from './utils';
+import { getPositionOfSelectedItem } from '@deephaven/react-hooks';
+import { PICKER_ITEM_HEIGHTS, PICKER_TOP_OFFSET } from '@deephaven/utils';
+import {
+ generateItemElements,
+ generateNormalizedItems,
+ sampleSectionIdAndClasses,
+} from './utils';
// Generate enough items to require scrolling
const items = [...generateNormalizedItems(52)];
+const itemElementsA = [...generateItemElements(0, 51)];
+const itemElementsB = [...generateItemElements(52, 103)];
+const itemElementsC = [...generateItemElements(104, 155)];
+const itemElementsD = [...generateItemElements(156, 207)];
+const itemElementsE = [...generateItemElements(208, 259)];
+
+const mixedItemsWithIconsNoDescriptions = [
+ 'String 1',
+ 'String 2',
+ 'String 3',
+ '',
+ 'Some really long text that should get truncated',
+ 444,
+ 999,
+ true,
+ false,
+ ...itemElementsA.map((itemEl, i) =>
+ i % 5 > 0
+ ? itemEl
+ : cloneElement(itemEl, {
+ ...itemEl.props,
+ children: [
+ ,
+
+ {itemEl.props.children}
+ ,
+ ],
+ })
+ ),
+];
function PersonIcon(): JSX.Element {
return (
@@ -26,6 +63,17 @@ function PersonIcon(): JSX.Element {
export function Pickers(): JSX.Element {
const [selectedKey, setSelectedKey] = useState(null);
+ const getInitialScrollPosition = useCallback(
+ async () =>
+ getPositionOfSelectedItem({
+ keyedItems: items,
+ itemHeight: PICKER_ITEM_HEIGHTS.medium,
+ selectedKey,
+ topOffset: PICKER_TOP_OFFSET,
+ }),
+ [selectedKey]
+ );
+
const onChange = useCallback((key: ItemKey): void => {
setSelectedKey(key);
}, []);
@@ -37,27 +85,11 @@ export function Pickers(): JSX.Element {
- - Aaa
+ - Aaa
-
- {/* eslint-disable react/jsx-curly-brace-presence */}
- {'String 1'}
- {'String 2'}
- {'String 3'}
- {''}
- {'Some really long text that should get truncated'}
- {/* eslint-enable react/jsx-curly-brace-presence */}
- {444}
- {999}
- {true}
- {false}
- - Item Aaa
- - Item Bbb
- -
-
- Complex Ccc with text that should be truncated
-
+
+ {mixedItemsWithIconsNoDescriptions}
@@ -65,41 +97,46 @@ export function Pickers(): JSX.Element {
{'String 1'}
{'String 2'}
{'String 3'}
-
- - Item Aaa
- - Item Bbb
+
+ - Item Aaa
+ - Item Bbb
-
Complex Ccc
- - Item Ddd
- - Item Eee
+ - Item Ddd
+ - Item Eee
-
Complex Fff
- -
+
-
Label
Description
- -
+
-
Label that causes overflow
Description that causes overflow
+
+
+
+
+
-
- {items}
-
+ />
);
diff --git a/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap b/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap
index a9964ea190..6ddf7d2bc4 100644
--- a/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap
+++ b/packages/code-studio/src/styleguide/__snapshots__/utils.test.ts.snap
@@ -6,6 +6,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "AAA",
"key": 100,
+ "textValue": "AAA",
},
"key": "A",
},
@@ -13,6 +14,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "BBB",
"key": 200,
+ "textValue": "BBB",
},
"key": "B",
},
@@ -20,6 +22,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "CCC",
"key": 300,
+ "textValue": "CCC",
},
"key": "C",
},
@@ -27,6 +30,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "DDD",
"key": 400,
+ "textValue": "DDD",
},
"key": "D",
},
@@ -34,6 +38,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "EEE",
"key": 500,
+ "textValue": "EEE",
},
"key": "E",
},
@@ -41,6 +46,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "FFF",
"key": 600,
+ "textValue": "FFF",
},
"key": "F",
},
@@ -48,6 +54,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "GGG",
"key": 700,
+ "textValue": "GGG",
},
"key": "G",
},
@@ -55,6 +62,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "HHH",
"key": 800,
+ "textValue": "HHH",
},
"key": "H",
},
@@ -62,6 +70,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "III",
"key": 900,
+ "textValue": "III",
},
"key": "I",
},
@@ -69,6 +78,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "JJJ",
"key": 1000,
+ "textValue": "JJJ",
},
"key": "J",
},
@@ -76,6 +86,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "KKK",
"key": 1100,
+ "textValue": "KKK",
},
"key": "K",
},
@@ -83,6 +94,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "LLL",
"key": 1200,
+ "textValue": "LLL",
},
"key": "L",
},
@@ -90,6 +102,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "MMM",
"key": 1300,
+ "textValue": "MMM",
},
"key": "M",
},
@@ -97,6 +110,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "NNN",
"key": 1400,
+ "textValue": "NNN",
},
"key": "N",
},
@@ -104,6 +118,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "OOO",
"key": 1500,
+ "textValue": "OOO",
},
"key": "O",
},
@@ -111,6 +126,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "PPP",
"key": 1600,
+ "textValue": "PPP",
},
"key": "P",
},
@@ -118,6 +134,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "QQQ",
"key": 1700,
+ "textValue": "QQQ",
},
"key": "Q",
},
@@ -125,6 +142,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "RRR",
"key": 1800,
+ "textValue": "RRR",
},
"key": "R",
},
@@ -132,6 +150,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "SSS",
"key": 1900,
+ "textValue": "SSS",
},
"key": "S",
},
@@ -139,6 +158,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "TTT",
"key": 2000,
+ "textValue": "TTT",
},
"key": "T",
},
@@ -146,6 +166,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "UUU",
"key": 2100,
+ "textValue": "UUU",
},
"key": "U",
},
@@ -153,6 +174,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "VVV",
"key": 2200,
+ "textValue": "VVV",
},
"key": "V",
},
@@ -160,6 +182,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "WWW",
"key": 2300,
+ "textValue": "WWW",
},
"key": "W",
},
@@ -167,6 +190,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "XXX",
"key": 2400,
+ "textValue": "XXX",
},
"key": "X",
},
@@ -174,6 +198,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "YYY",
"key": 2500,
+ "textValue": "YYY",
},
"key": "Y",
},
@@ -181,6 +206,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ZZZ",
"key": 2600,
+ "textValue": "ZZZ",
},
"key": "Z",
},
@@ -188,6 +214,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "aaa",
"key": 2700,
+ "textValue": "aaa",
},
"key": "a",
},
@@ -195,6 +222,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "bbb",
"key": 2800,
+ "textValue": "bbb",
},
"key": "b",
},
@@ -202,6 +230,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ccc",
"key": 2900,
+ "textValue": "ccc",
},
"key": "c",
},
@@ -209,6 +238,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ddd",
"key": 3000,
+ "textValue": "ddd",
},
"key": "d",
},
@@ -216,6 +246,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "eee",
"key": 3100,
+ "textValue": "eee",
},
"key": "e",
},
@@ -223,6 +254,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "fff",
"key": 3200,
+ "textValue": "fff",
},
"key": "f",
},
@@ -230,6 +262,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ggg",
"key": 3300,
+ "textValue": "ggg",
},
"key": "g",
},
@@ -237,6 +270,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "hhh",
"key": 3400,
+ "textValue": "hhh",
},
"key": "h",
},
@@ -244,6 +278,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "iii",
"key": 3500,
+ "textValue": "iii",
},
"key": "i",
},
@@ -251,6 +286,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "jjj",
"key": 3600,
+ "textValue": "jjj",
},
"key": "j",
},
@@ -258,6 +294,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "kkk",
"key": 3700,
+ "textValue": "kkk",
},
"key": "k",
},
@@ -265,6 +302,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "lll",
"key": 3800,
+ "textValue": "lll",
},
"key": "l",
},
@@ -272,6 +310,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "mmm",
"key": 3900,
+ "textValue": "mmm",
},
"key": "m",
},
@@ -279,6 +318,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "nnn",
"key": 4000,
+ "textValue": "nnn",
},
"key": "n",
},
@@ -286,6 +326,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ooo",
"key": 4100,
+ "textValue": "ooo",
},
"key": "o",
},
@@ -293,6 +334,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ppp",
"key": 4200,
+ "textValue": "ppp",
},
"key": "p",
},
@@ -300,6 +342,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "qqq",
"key": 4300,
+ "textValue": "qqq",
},
"key": "q",
},
@@ -307,6 +350,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "rrr",
"key": 4400,
+ "textValue": "rrr",
},
"key": "r",
},
@@ -314,6 +358,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "sss",
"key": 4500,
+ "textValue": "sss",
},
"key": "s",
},
@@ -321,6 +366,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ttt",
"key": 4600,
+ "textValue": "ttt",
},
"key": "t",
},
@@ -328,6 +374,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "uuu",
"key": 4700,
+ "textValue": "uuu",
},
"key": "u",
},
@@ -335,6 +382,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "vvv",
"key": 4800,
+ "textValue": "vvv",
},
"key": "v",
},
@@ -342,6 +390,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "www",
"key": 4900,
+ "textValue": "www",
},
"key": "w",
},
@@ -349,6 +398,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "xxx",
"key": 5000,
+ "textValue": "xxx",
},
"key": "x",
},
@@ -356,6 +406,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "yyy",
"key": 5100,
+ "textValue": "yyy",
},
"key": "y",
},
@@ -363,6 +414,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "zzz",
"key": 5200,
+ "textValue": "zzz",
},
"key": "z",
},
@@ -370,6 +422,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "AAA1",
"key": 5300,
+ "textValue": "AAA1",
},
"key": "A1",
},
@@ -377,6 +430,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "BBB1",
"key": 5400,
+ "textValue": "BBB1",
},
"key": "B1",
},
@@ -384,6 +438,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "CCC1",
"key": 5500,
+ "textValue": "CCC1",
},
"key": "C1",
},
@@ -391,6 +446,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "DDD1",
"key": 5600,
+ "textValue": "DDD1",
},
"key": "D1",
},
@@ -398,6 +454,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "EEE1",
"key": 5700,
+ "textValue": "EEE1",
},
"key": "E1",
},
@@ -405,6 +462,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "FFF1",
"key": 5800,
+ "textValue": "FFF1",
},
"key": "F1",
},
@@ -412,6 +470,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "GGG1",
"key": 5900,
+ "textValue": "GGG1",
},
"key": "G1",
},
@@ -419,6 +478,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "HHH1",
"key": 6000,
+ "textValue": "HHH1",
},
"key": "H1",
},
@@ -426,6 +486,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "III1",
"key": 6100,
+ "textValue": "III1",
},
"key": "I1",
},
@@ -433,6 +494,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "JJJ1",
"key": 6200,
+ "textValue": "JJJ1",
},
"key": "J1",
},
@@ -440,6 +502,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "KKK1",
"key": 6300,
+ "textValue": "KKK1",
},
"key": "K1",
},
@@ -447,6 +510,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "LLL1",
"key": 6400,
+ "textValue": "LLL1",
},
"key": "L1",
},
@@ -454,6 +518,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "MMM1",
"key": 6500,
+ "textValue": "MMM1",
},
"key": "M1",
},
@@ -461,6 +526,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "NNN1",
"key": 6600,
+ "textValue": "NNN1",
},
"key": "N1",
},
@@ -468,6 +534,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "OOO1",
"key": 6700,
+ "textValue": "OOO1",
},
"key": "O1",
},
@@ -475,6 +542,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "PPP1",
"key": 6800,
+ "textValue": "PPP1",
},
"key": "P1",
},
@@ -482,6 +550,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "QQQ1",
"key": 6900,
+ "textValue": "QQQ1",
},
"key": "Q1",
},
@@ -489,6 +558,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "RRR1",
"key": 7000,
+ "textValue": "RRR1",
},
"key": "R1",
},
@@ -496,6 +566,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "SSS1",
"key": 7100,
+ "textValue": "SSS1",
},
"key": "S1",
},
@@ -503,6 +574,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "TTT1",
"key": 7200,
+ "textValue": "TTT1",
},
"key": "T1",
},
@@ -510,6 +582,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "UUU1",
"key": 7300,
+ "textValue": "UUU1",
},
"key": "U1",
},
@@ -517,6 +590,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "VVV1",
"key": 7400,
+ "textValue": "VVV1",
},
"key": "V1",
},
@@ -524,6 +598,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "WWW1",
"key": 7500,
+ "textValue": "WWW1",
},
"key": "W1",
},
@@ -531,6 +606,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "XXX1",
"key": 7600,
+ "textValue": "XXX1",
},
"key": "X1",
},
@@ -538,6 +614,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "YYY1",
"key": 7700,
+ "textValue": "YYY1",
},
"key": "Y1",
},
@@ -545,6 +622,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ZZZ1",
"key": 7800,
+ "textValue": "ZZZ1",
},
"key": "Z1",
},
@@ -552,6 +630,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "aaa1",
"key": 7900,
+ "textValue": "aaa1",
},
"key": "a1",
},
@@ -559,6 +638,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "bbb1",
"key": 8000,
+ "textValue": "bbb1",
},
"key": "b1",
},
@@ -566,6 +646,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ccc1",
"key": 8100,
+ "textValue": "ccc1",
},
"key": "c1",
},
@@ -573,6 +654,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ddd1",
"key": 8200,
+ "textValue": "ddd1",
},
"key": "d1",
},
@@ -580,6 +662,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "eee1",
"key": 8300,
+ "textValue": "eee1",
},
"key": "e1",
},
@@ -587,6 +670,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "fff1",
"key": 8400,
+ "textValue": "fff1",
},
"key": "f1",
},
@@ -594,6 +678,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ggg1",
"key": 8500,
+ "textValue": "ggg1",
},
"key": "g1",
},
@@ -601,6 +686,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "hhh1",
"key": 8600,
+ "textValue": "hhh1",
},
"key": "h1",
},
@@ -608,6 +694,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "iii1",
"key": 8700,
+ "textValue": "iii1",
},
"key": "i1",
},
@@ -615,6 +702,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "jjj1",
"key": 8800,
+ "textValue": "jjj1",
},
"key": "j1",
},
@@ -622,6 +710,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "kkk1",
"key": 8900,
+ "textValue": "kkk1",
},
"key": "k1",
},
@@ -629,6 +718,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "lll1",
"key": 9000,
+ "textValue": "lll1",
},
"key": "l1",
},
@@ -636,6 +726,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "mmm1",
"key": 9100,
+ "textValue": "mmm1",
},
"key": "m1",
},
@@ -643,6 +734,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "nnn1",
"key": 9200,
+ "textValue": "nnn1",
},
"key": "n1",
},
@@ -650,6 +742,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ooo1",
"key": 9300,
+ "textValue": "ooo1",
},
"key": "o1",
},
@@ -657,6 +750,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ppp1",
"key": 9400,
+ "textValue": "ppp1",
},
"key": "p1",
},
@@ -664,6 +758,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "qqq1",
"key": 9500,
+ "textValue": "qqq1",
},
"key": "q1",
},
@@ -671,6 +766,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "rrr1",
"key": 9600,
+ "textValue": "rrr1",
},
"key": "r1",
},
@@ -678,6 +774,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "sss1",
"key": 9700,
+ "textValue": "sss1",
},
"key": "s1",
},
@@ -685,6 +782,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "ttt1",
"key": 9800,
+ "textValue": "ttt1",
},
"key": "t1",
},
@@ -692,6 +790,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "uuu1",
"key": 9900,
+ "textValue": "uuu1",
},
"key": "u1",
},
@@ -699,6 +798,7 @@ exports[`generateNormalizedItems should generate normalized items 1`] = `
"item": {
"content": "vvv1",
"key": 10000,
+ "textValue": "vvv1",
},
"key": "v1",
},
diff --git a/packages/code-studio/src/styleguide/utils.ts b/packages/code-studio/src/styleguide/utils.ts
index 7e7f9087e3..e3a778d764 100644
--- a/packages/code-studio/src/styleguide/utils.ts
+++ b/packages/code-studio/src/styleguide/utils.ts
@@ -1,10 +1,39 @@
import cl from 'classnames';
-import { useCallback, useState } from 'react';
-import { NormalizedItem } from '@deephaven/components';
+import { createElement, useCallback, useState } from 'react';
+import { Item, ItemElement, NormalizedItem } from '@deephaven/components';
export const HIDE_FROM_E2E_TESTS_CLASS = 'hide-from-e2e-tests';
export const SAMPLE_SECTION_CLASS = 'sample-section';
+/**
+ * Generate a given number of `Item` elements.
+ */
+export function* generateItemElements(
+ start: number,
+ end: number
+): Generator {
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+ const len = letters.length;
+
+ for (let i = start; i <= end; i += 1) {
+ const charI = i % len;
+ let suffix = String(Math.floor(i / len));
+ if (suffix === '0') {
+ suffix = '';
+ }
+ const letter = letters[charI];
+ const key = `${letter}${suffix}`;
+ const content = `${letter.repeat(3)}${suffix}`;
+
+ // eslint-disable-next-line react/no-children-prop
+ yield createElement(Item, {
+ key,
+ textValue: content,
+ children: content,
+ });
+ }
+}
+
/**
* Generate a given number of NormalizedItems.
* @param count The number of items to generate
@@ -23,12 +52,14 @@ export function* generateNormalizedItems(
}
const letter = letters[charI];
const key = `${letter}${suffix}`;
+ const content = `${letter.repeat(3)}${suffix}`;
yield {
key,
item: {
key: (i + 1) * 100,
- content: `${letter}${letter}${letter}${suffix}`,
+ content,
+ textValue: content,
},
};
}
diff --git a/packages/components/src/spectrum/ItemContent.tsx b/packages/components/src/spectrum/ItemContent.tsx
index 1526d3ed64..f11e6c9638 100644
--- a/packages/components/src/spectrum/ItemContent.tsx
+++ b/packages/components/src/spectrum/ItemContent.tsx
@@ -7,6 +7,7 @@ import {
} from 'react';
import cl from 'classnames';
import { isElementOfType, useCheckOverflow } from '@deephaven/react-hooks';
+import { NON_BREAKING_SPACE } from '@deephaven/utils';
import { Text } from './Text';
import { TooltipOptions } from './utils';
import ItemTooltip from './ItemTooltip';
@@ -46,7 +47,7 @@ export function ItemContent({
/* eslint-disable no-param-reassign */
if (content === '') {
// Prevent the item height from collapsing when the content is empty
- content = '\xa0'; // Non-breaking space
+ content = NON_BREAKING_SPACE;
} else if (typeof content === 'boolean') {
// Boolean values need to be stringified to render
content = String(content);
diff --git a/packages/components/src/spectrum/picker/Picker.tsx b/packages/components/src/spectrum/picker/Picker.tsx
index d4bba67aa8..f9fcf64263 100644
--- a/packages/components/src/spectrum/picker/Picker.tsx
+++ b/packages/components/src/spectrum/picker/Picker.tsx
@@ -1,41 +1,38 @@
-import { useCallback, useMemo } from 'react';
-import { DOMRef } from '@react-types/shared';
+import { useCallback, useMemo, useState } from 'react';
+import type { DOMRef } from '@react-types/shared';
import { Picker as SpectrumPicker } from '@adobe/react-spectrum';
-import {
- getPositionOfSelectedItem,
- findSpectrumPickerScrollArea,
- usePopoverOnScrollRef,
-} from '@deephaven/react-hooks';
+import cl from 'classnames';
import {
EMPTY_FUNCTION,
- PICKER_ITEM_HEIGHT,
+ PICKER_ITEM_HEIGHTS,
PICKER_TOP_OFFSET,
} from '@deephaven/utils';
-import cl from 'classnames';
import {
- isNormalizedSection,
NormalizedSpectrumPickerProps,
- normalizeItemList,
- normalizeTooltipOptions,
- NormalizedItem,
ItemOrSection,
- TooltipOptions,
+ getPositionOfSelectedItemElement,
ItemKey,
- getItemKey,
+ normalizeTooltipOptions,
+ TooltipOptions,
+ isItemElementWithDescription,
+ isSectionElement,
} from '../utils/itemUtils';
-import { Section } from '../shared';
-import { useRenderNormalizedItem } from '../utils';
+import { wrapItemChildren } from '../utils/itemWrapperUtils';
+import usePickerScrollOnOpen from './usePickerScrollOnOpen';
+import { useSpectrumThemeProvider } from '../../theme';
export type PickerProps = {
- children: ItemOrSection | ItemOrSection[] | NormalizedItem[];
+ children: ItemOrSection | ItemOrSection[];
+
/** Can be set to true or a TooltipOptions to enable item tooltips */
tooltip?: boolean | TooltipOptions;
+
/** The currently selected key in the collection (controlled). */
selectedKey?: ItemKey | null;
+
/** The initial selected key in the collection (uncontrolled). */
defaultSelectedKey?: ItemKey;
- /** Function to retrieve initial scroll position when opening the picker */
- getInitialScrollPosition?: () => Promise;
+
/**
* Handler that is called when the selection change.
* Note that under the hood, this is just an alias for Spectrum's
@@ -68,9 +65,9 @@ export type PickerProps = {
/**
* Picker component for selecting items from a list of items. Items can be
- * provided via the `items` prop or as children. Each item can be a string,
- * number, boolean, or a Spectrum - element. The remaining props are just
- * pass through props for the Spectrum Picker component.
+ * provided via the `children` prop. Each item can be a string, number, boolean,
+ * or a Spectrum
- element. The remaining props are just pass through props
+ * for the Spectrum Picker component.
* See https://react-spectrum.adobe.com/react-spectrum/Picker.html
*/
export function Picker({
@@ -78,7 +75,6 @@ export function Picker({
tooltip = true,
defaultSelectedKey,
selectedKey,
- getInitialScrollPosition,
onChange,
onOpenChange,
onScroll = EMPTY_FUNCTION,
@@ -87,104 +83,90 @@ export function Picker({
UNSAFE_className,
...spectrumPickerProps
}: PickerProps): JSX.Element {
- const normalizedItems = useMemo(
- () => normalizeItemList(children),
- [children]
- );
+ const { scale } = useSpectrumThemeProvider();
+ const itemHeight = PICKER_ITEM_HEIGHTS[scale];
const tooltipOptions = useMemo(
() => normalizeTooltipOptions(tooltip),
[tooltip]
);
- const renderNormalizedItem = useRenderNormalizedItem(tooltipOptions);
+ // `null` is a valid value for `selectedKey` in controlled mode, so we check
+ // for explicit `undefined` to identify uncontrolled mode.
+ const isUncontrolled = selectedKey === undefined;
+ const [uncontrolledSelectedKey, setUncontrolledSelectedKey] =
+ useState(defaultSelectedKey);
- const getInitialScrollPositionInternal = useCallback(
- () =>
- getInitialScrollPosition == null
- ? getPositionOfSelectedItem({
- keyedItems: normalizedItems,
- // TODO: #1890 & deephaven-plugins#371 add support for sections and
- // items with descriptions since they impact the height calculations
- itemHeight: PICKER_ITEM_HEIGHT,
- selectedKey,
- topOffset: PICKER_TOP_OFFSET,
- })
- : getInitialScrollPosition(),
- [getInitialScrollPosition, normalizedItems, selectedKey]
+ const wrappedItems = useMemo(
+ () => wrapItemChildren(children, tooltipOptions),
+ [children, tooltipOptions]
);
- const { ref: scrollRef, onOpenChange: popoverOnOpenChange } =
- usePopoverOnScrollRef(
- findSpectrumPickerScrollArea,
- onScroll,
- getInitialScrollPositionInternal
- );
-
- const onOpenChangeInternal = useCallback(
- (isOpen: boolean): void => {
- // Attach scroll event handling
- popoverOnOpenChange(isOpen);
+ // Item descriptions and Section elements introduce variable item heights.
+ // This throws off scroll position calculations, so we disable auto scrolling
+ // if either of these are found.
+ const disableScrollOnOpen = useMemo(
+ () =>
+ wrappedItems.some(
+ item => isSectionElement(item) || isItemElementWithDescription(item)
+ ),
+ [wrappedItems]
+ );
- onOpenChange?.(isOpen);
- },
- [onOpenChange, popoverOnOpenChange]
+ const getInitialScrollPosition = useCallback(
+ async () =>
+ disableScrollOnOpen
+ ? null
+ : getPositionOfSelectedItemElement({
+ items: wrappedItems,
+ itemHeight,
+ selectedKey: isUncontrolled ? uncontrolledSelectedKey : selectedKey,
+ topOffset: PICKER_TOP_OFFSET,
+ }),
+ [
+ disableScrollOnOpen,
+ isUncontrolled,
+ itemHeight,
+ selectedKey,
+ uncontrolledSelectedKey,
+ wrappedItems,
+ ]
);
const onSelectionChangeInternal = useCallback(
(key: ItemKey): void => {
- // The `key` arg will always be a string due to us setting the `Item` key
- // prop in `renderItem`. We need to find the matching item to determine
- // the actual key.
- const selectedItem = normalizedItems.find(
- item => String(getItemKey(item)) === key
- );
-
- const actualKey = getItemKey(selectedItem) ?? key;
+ // If our component is uncontrolled, track the selected key internally
+ // so that we can scroll to the selected item if the user re-opens
+ if (isUncontrolled) {
+ setUncontrolledSelectedKey(key);
+ }
- (onChange ?? onSelectionChange)?.(actualKey);
+ (onChange ?? onSelectionChange)?.(key);
},
- [normalizedItems, onChange, onSelectionChange]
+ [isUncontrolled, onChange, onSelectionChange]
);
+ const { ref: scrollRef, onOpenChange: onOpenChangeInternal } =
+ usePickerScrollOnOpen({
+ getInitialScrollPosition,
+ onScroll,
+ onOpenChange,
+ });
+
return (
}
- onOpenChange={onOpenChangeInternal}
+ ref={scrollRef as DOMRef}
UNSAFE_className={cl('dh-picker', UNSAFE_className)}
- items={normalizedItems}
- // Spectrum Picker treats keys as strings if the `key` prop is explicitly
- // set on `Item` elements. Since we do this in `renderItem`, we need to
- // ensure that `selectedKey` and `defaultSelectedKey` are strings in order
- // for selection to work.
- selectedKey={selectedKey == null ? selectedKey : selectedKey.toString()}
+ selectedKey={selectedKey as NormalizedSpectrumPickerProps['selectedKey']}
defaultSelectedKey={
- defaultSelectedKey == null
- ? defaultSelectedKey
- : defaultSelectedKey.toString()
- }
- // `onChange` is just an alias for `onSelectionChange`
- onSelectionChange={
- onSelectionChangeInternal as NormalizedSpectrumPickerProps['onSelectionChange']
+ defaultSelectedKey as NormalizedSpectrumPickerProps['defaultSelectedKey']
}
+ onSelectionChange={onSelectionChangeInternal}
+ onOpenChange={onOpenChangeInternal}
>
- {itemOrSection => {
- if (isNormalizedSection(itemOrSection)) {
- return (
-
- {renderNormalizedItem}
-
- );
- }
-
- return renderNormalizedItem(itemOrSection);
- }}
+ {wrappedItems}
);
}
diff --git a/packages/components/src/spectrum/picker/PickerNormalized.tsx b/packages/components/src/spectrum/picker/PickerNormalized.tsx
new file mode 100644
index 0000000000..9c1d17bf5f
--- /dev/null
+++ b/packages/components/src/spectrum/picker/PickerNormalized.tsx
@@ -0,0 +1,112 @@
+import { useMemo } from 'react';
+import { Picker as SpectrumPicker } from '@adobe/react-spectrum';
+import type { DOMRef } from '@react-types/shared';
+import cl from 'classnames';
+import { EMPTY_FUNCTION } from '@deephaven/utils';
+import { Section } from '../shared';
+import type { PickerProps as PickerBaseProps } from './Picker';
+
+import {
+ getItemKey,
+ isNormalizedSection,
+ NormalizedItem,
+ NormalizedSection,
+ normalizeTooltipOptions,
+ useRenderNormalizedItem,
+ useStringifiedSelection,
+} from '../utils';
+import usePickerScrollOnOpen from './usePickerScrollOnOpen';
+
+export interface PickerNormalizedProps
+ extends Omit {
+ normalizedItems: (NormalizedItem | NormalizedSection)[];
+ getInitialScrollPosition?: () => Promise;
+ onScroll?: (event: Event) => void;
+}
+
+/**
+ * Picker that takes an array of `NormalizedItem` or `NormalizedSection` items
+ * as children and uses a render item function to render the items. This is
+ * necessary to support windowed data.
+ */
+export function PickerNormalized({
+ normalizedItems,
+ tooltip = true,
+ selectedKey,
+ defaultSelectedKey,
+ disabledKeys,
+ UNSAFE_className,
+ getInitialScrollPosition,
+ onChange,
+ onOpenChange,
+ onScroll = EMPTY_FUNCTION,
+ onSelectionChange,
+ ...props
+}: PickerNormalizedProps): JSX.Element {
+ const tooltipOptions = useMemo(
+ () => normalizeTooltipOptions(tooltip),
+ [tooltip]
+ );
+
+ const renderNormalizedItem = useRenderNormalizedItem(tooltipOptions);
+
+ const { ref: scrollRef, onOpenChange: onOpenChangeInternal } =
+ usePickerScrollOnOpen({
+ getInitialScrollPosition,
+ onScroll,
+ onOpenChange,
+ });
+
+ // Spectrum Picker treats keys as strings if the `key` prop is explicitly
+ // set on `Item` elements. Since we do this in `renderItem`, we need to
+ // map original key types to and from strings so that selection works.
+ const {
+ selectedStringKey,
+ defaultSelectedStringKey,
+ disabledStringKeys,
+ onStringSelectionChange,
+ } = useStringifiedSelection({
+ normalizedItems,
+ selectedKey,
+ defaultSelectedKey,
+ disabledKeys,
+ onChange: onChange ?? onSelectionChange,
+ });
+
+ return (
+ }
+ UNSAFE_className={cl(
+ 'dh-picker',
+ 'dh-picker-normalized',
+ UNSAFE_className
+ )}
+ items={normalizedItems}
+ selectedKey={selectedStringKey}
+ defaultSelectedKey={defaultSelectedStringKey}
+ disabledKeys={disabledStringKeys}
+ onSelectionChange={onStringSelectionChange}
+ onOpenChange={onOpenChangeInternal}
+ >
+ {itemOrSection => {
+ if (isNormalizedSection(itemOrSection)) {
+ return (
+
+ {renderNormalizedItem}
+
+ );
+ }
+
+ return renderNormalizedItem(itemOrSection);
+ }}
+
+ );
+}
+
+export default PickerNormalized;
diff --git a/packages/components/src/spectrum/picker/index.ts b/packages/components/src/spectrum/picker/index.ts
index c434d5d810..2ce75f0446 100644
--- a/packages/components/src/spectrum/picker/index.ts
+++ b/packages/components/src/spectrum/picker/index.ts
@@ -1 +1,3 @@
export * from './Picker';
+export * from './PickerNormalized';
+export * from './usePickerScrollOnOpen';
diff --git a/packages/components/src/spectrum/picker/usePickerScrollOnOpen.test.ts b/packages/components/src/spectrum/picker/usePickerScrollOnOpen.test.ts
new file mode 100644
index 0000000000..fa829ce5d2
--- /dev/null
+++ b/packages/components/src/spectrum/picker/usePickerScrollOnOpen.test.ts
@@ -0,0 +1,71 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { TestUtils } from '@deephaven/utils';
+import {
+ findSpectrumPickerScrollArea,
+ usePopoverOnScrollRef,
+ UsePopoverOnScrollRefResult,
+} from '@deephaven/react-hooks';
+import { usePickerScrollOnOpen } from './usePickerScrollOnOpen';
+
+const { asMock, createMockProxy } = TestUtils;
+
+jest.mock('@deephaven/react-hooks');
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ expect.hasAssertions();
+});
+
+describe('usePickerScrollOnOpen', () => {
+ const getInitialScrollPosition = jest
+ .fn()
+ .mockName('getInitialScrollPosition');
+ const onScroll = jest.fn().mockName('onScroll');
+ const onOpenChange = jest.fn().mockName('onOpenChange');
+
+ const mockUsePopoverOnScrollRefResult =
+ createMockProxy>();
+
+ beforeEach(() => {
+ asMock(usePopoverOnScrollRef)
+ .mockName('usePopoverOnScrollRef')
+ .mockReturnValue(mockUsePopoverOnScrollRefResult);
+ });
+
+ it('should return ref from usePopoverOnScrollRef', () => {
+ const { result } = renderHook(() =>
+ usePickerScrollOnOpen({
+ getInitialScrollPosition,
+ onScroll,
+ onOpenChange,
+ })
+ );
+
+ expect(usePopoverOnScrollRef).toHaveBeenCalledWith(
+ findSpectrumPickerScrollArea,
+ onScroll,
+ getInitialScrollPosition
+ );
+ expect(result.current.ref).toBe(mockUsePopoverOnScrollRefResult.ref);
+ });
+
+ it.each([true, false])(
+ 'should return a callback that calls popoverOnOpenChange and onOpenChange: %s',
+ isOpen => {
+ const { result } = renderHook(() =>
+ usePickerScrollOnOpen({
+ getInitialScrollPosition,
+ onScroll,
+ onOpenChange,
+ })
+ );
+
+ result.current.onOpenChange(isOpen);
+
+ expect(mockUsePopoverOnScrollRefResult.onOpenChange).toHaveBeenCalledWith(
+ isOpen
+ );
+ expect(onOpenChange).toHaveBeenCalledWith(isOpen);
+ }
+ );
+});
diff --git a/packages/components/src/spectrum/picker/usePickerScrollOnOpen.ts b/packages/components/src/spectrum/picker/usePickerScrollOnOpen.ts
new file mode 100644
index 0000000000..d9ad3114f4
--- /dev/null
+++ b/packages/components/src/spectrum/picker/usePickerScrollOnOpen.ts
@@ -0,0 +1,55 @@
+import { useCallback } from 'react';
+import type { DOMRef } from '@react-types/shared';
+import {
+ findSpectrumPickerScrollArea,
+ usePopoverOnScrollRef,
+} from '@deephaven/react-hooks';
+
+export interface UsePickerScrollOnOpenOptions {
+ getInitialScrollPosition?: () => Promise;
+ onScroll: (event: Event) => void;
+ onOpenChange?: (isOpen: boolean) => void;
+}
+
+export interface UsePickerScrollOnOpenResult {
+ ref: DOMRef;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+/**
+ * Handle scroll event registration and scrolling to initial scroll position
+ * whenever a Picker popover is opened.
+ * @param getInitialScrollPosition Function to get the initial scroll position.
+ * @param onScroll Callback for scroll events.
+ * @param onOpenChange Callback for open change events.
+ * @return A ref to attach to the Picker and a callback to handle open change
+ * events for the Picker.
+ */
+export function usePickerScrollOnOpen({
+ getInitialScrollPosition,
+ onScroll,
+ onOpenChange,
+}: UsePickerScrollOnOpenOptions): UsePickerScrollOnOpenResult {
+ const { ref, onOpenChange: popoverOnOpenChange } = usePopoverOnScrollRef(
+ findSpectrumPickerScrollArea,
+ onScroll,
+ getInitialScrollPosition
+ );
+
+ const onOpenChangeInternal = useCallback(
+ (isOpen: boolean): void => {
+ // Attach scroll event handling
+ popoverOnOpenChange(isOpen);
+
+ onOpenChange?.(isOpen);
+ },
+ [onOpenChange, popoverOnOpenChange]
+ );
+
+ return {
+ ref,
+ onOpenChange: onOpenChangeInternal,
+ };
+}
+
+export default usePickerScrollOnOpen;
diff --git a/packages/components/src/spectrum/utils/index.ts b/packages/components/src/spectrum/utils/index.ts
index ef406aba98..e70285dc86 100644
--- a/packages/components/src/spectrum/utils/index.ts
+++ b/packages/components/src/spectrum/utils/index.ts
@@ -1,4 +1,6 @@
export * from './itemUtils';
+export * from './itemWrapperUtils';
export * from './themeUtils';
export * from './useRenderNormalizedItem';
export * from './useStringifiedMultiSelection';
+export * from './useStringifiedSelection';
diff --git a/packages/components/src/spectrum/utils/itemUtils.test.tsx b/packages/components/src/spectrum/utils/itemUtils.test.tsx
index 0585cfb8c8..ad4a985496 100644
--- a/packages/components/src/spectrum/utils/itemUtils.test.tsx
+++ b/packages/components/src/spectrum/utils/itemUtils.test.tsx
@@ -15,10 +15,13 @@ import {
ItemOrSection,
SectionElement,
itemSelectionToStringSet,
+ getPositionOfSelectedItemElement,
+ isItemElementWithDescription,
} from './itemUtils';
import type { PickerProps } from '../picker/Picker';
import { Item, Section } from '../shared';
import { Text } from '../Text';
+import ItemContent from '../ItemContent';
beforeEach(() => {
expect.hasAssertions();
@@ -76,7 +79,10 @@ const expectedItems = {
No textValue
,
{
- item: { content: No textValue },
+ item: {
+ content: No textValue,
+ textValue: undefined,
+ },
},
],
explicitKey: [
@@ -171,6 +177,111 @@ describe('getItemKey', () => {
);
});
+describe('getPositionOfSelectedItemElement', () => {
+ const items = [
+ - A
,
+ - B
,
+ - C
,
+ ];
+ const itemHeight = 40;
+ const topOffset = 5;
+
+ it.each([null, undefined])(
+ 'should return top offset if selectedKey is not defined: %s',
+ async selectedKey => {
+ const actual = await getPositionOfSelectedItemElement({
+ items,
+ itemHeight,
+ selectedKey,
+ topOffset,
+ });
+
+ expect(actual).toEqual(topOffset);
+ }
+ );
+
+ it('should return top offset if selectedKey is not found', async () => {
+ const selectedKey = '4';
+
+ const actual = await getPositionOfSelectedItemElement({
+ items,
+ itemHeight,
+ selectedKey,
+ topOffset,
+ });
+
+ expect(actual).toEqual(topOffset);
+ });
+
+ it.each(['1', '2', '3'])(
+ 'should return the position of the selected item element: %s',
+ async selectedKey => {
+ const expected = (Number(selectedKey) - 1) * itemHeight + topOffset;
+
+ const actual = await getPositionOfSelectedItemElement({
+ items,
+ itemHeight,
+ selectedKey,
+ topOffset,
+ });
+
+ expect(actual).toEqual(expected);
+ }
+ );
+});
+
+describe('isItemElementWithDescription', () => {
+ it.each([
+ [
+ 'Item with description',
+ true,
+ -
+ Label
+ Description
+
,
+ ],
+ [
+ 'ItemContent with description',
+ true,
+ -
+
+ Label
+ Description
+
+
,
+ ],
+ [
+ 'Section with Item description',
+ false,
+
+ -
+ Label
+ Description
+
+ ,
+ ],
+ [
+ 'Item no description',
+ false,
+ -
+ Label
+
,
+ ],
+ [
+ 'ItemContent no description',
+ false,
+ -
+
+ Label
+
+
,
+ ],
+ ])(`%s should return %s`, (_label, expected, node) => {
+ const actual = isItemElementWithDescription(node);
+ expect(actual).toEqual(expected);
+ });
+});
+
describe('isNormalizedItemsWithKeysList', () => {
const mock = {
normalizedItemWithKey: {
diff --git a/packages/components/src/spectrum/utils/itemUtils.ts b/packages/components/src/spectrum/utils/itemUtils.ts
index 351d13f94d..a103ea03fe 100644
--- a/packages/components/src/spectrum/utils/itemUtils.ts
+++ b/packages/components/src/spectrum/utils/itemUtils.ts
@@ -1,13 +1,22 @@
-import { isValidElement, Key, ReactElement, ReactNode } from 'react';
+import { Key, ReactElement, ReactNode } from 'react';
import { SpectrumPickerProps } from '@adobe/react-spectrum';
import type { ItemRenderer } from '@react-types/shared';
import Log from '@deephaven/log';
+import { isElementOfType } from '@deephaven/react-hooks';
import { KeyedItem, SelectionT } from '@deephaven/utils';
import { Item, ItemProps, Section, SectionProps } from '../shared';
import { PopperOptions } from '../../popper';
+import { Text } from '../Text';
+import ItemContent from '../ItemContent';
const log = Log.module('itemUtils');
+/**
+ * `Item.textValue` prop needs to be a non-empty string for accessibility
+ * purposes. This is not displayed in the UI.
+ */
+export const ITEM_EMPTY_STRING_TEXT_VALUE = 'Empty';
+
export const INVALID_ITEM_ERROR_MESSAGE =
'Items must be strings, numbers, booleans, - or elements:';
@@ -20,7 +29,7 @@ type SectionPropsNoItemRenderer = Omit, 'children'> & {
children: Exclude['children'], ItemRenderer>;
};
-type ItemElement = ReactElement>;
+export type ItemElement = ReactElement>;
export type SectionElement = ReactElement>;
export type ItemElementOrPrimitive = number | string | boolean | ItemElement;
@@ -45,7 +54,7 @@ export type ItemSelectionChangeHandler = (key: ItemKey) => void;
export interface NormalizedItemData {
key?: ItemKey;
content: ReactNode;
- textValue?: string;
+ textValue: string | undefined;
}
export interface NormalizedSectionData {
@@ -96,6 +105,45 @@ export function getItemKey<
return (item?.item?.key ?? item?.key) as TKey;
}
+/**
+ * Get the position of the item with the given selected key in a list of items.
+ * @param items The items to search
+ * @param itemHeight The height of each item
+ * @param selectedKey The key of the selected item
+ * @param topOffset The top offset of the list
+ * @returns The position of the selected item or the top offset if not found
+ */
+export async function getPositionOfSelectedItemElement<
+ TKey extends string | number | boolean | undefined,
+>({
+ items,
+ itemHeight,
+ selectedKey,
+ topOffset,
+}: {
+ items: ItemElement[];
+ selectedKey: TKey | null | undefined;
+ itemHeight: number;
+ topOffset: number;
+}): Promise {
+ let position = topOffset;
+
+ if (selectedKey == null) {
+ return position;
+ }
+
+ for (let i = 0; i < items.length; i += 1) {
+ const item = items[i];
+ if (item.key === selectedKey) {
+ return position;
+ }
+
+ position += itemHeight;
+ }
+
+ return topOffset;
+}
+
/**
* Determine if a node is a Section element.
* @param node The node to check
@@ -104,7 +152,7 @@ export function getItemKey<
export function isSectionElement(
node: ReactNode
): node is ReactElement> {
- return isValidElement>(node) && node.type === Section;
+ return isElementOfType(node, Section);
}
/**
@@ -115,7 +163,34 @@ export function isSectionElement(
export function isItemElement(
node: ReactNode
): node is ReactElement> {
- return isValidElement>(node) && node.type === Item;
+ return isElementOfType(node, Item);
+}
+
+/**
+ * Determine if a node is an Item element containing a child `Text` element with
+ * a `slot` prop set to `description`.
+ * @param node The node to check
+ * @returns True if the node is an Item element with a description
+ */
+export function isItemElementWithDescription(
+ node: ReactNode
+): node is ReactElement> {
+ if (!isItemElement(node)) {
+ return false;
+ }
+
+ // If children are wrapped in `ItemContent`, go down 1 level
+ const children = isElementOfType(node.props.children, ItemContent)
+ ? node.props.children.props.children
+ : node.props.children;
+
+ const childrenArray = Array.isArray(children) ? children : [children];
+
+ const result = childrenArray.some(
+ child => child.props?.slot === 'description' && isElementOfType(child, Text)
+ );
+
+ return result;
}
/**
@@ -315,12 +390,17 @@ export function normalizeTooltipOptions(
* @param itemKeys The selection of `ItemKey`s
* @returns The selection of strings
*/
-export function itemSelectionToStringSet(
- itemKeys?: 'all' | Iterable
-): undefined | 'all' | Set {
+export function itemSelectionToStringSet<
+ TKeys extends 'all' | Iterable | undefined,
+ TResult extends TKeys extends 'all'
+ ? 'all'
+ : TKeys extends Iterable
+ ? Set
+ : undefined,
+>(itemKeys: TKeys): TResult {
if (itemKeys == null || itemKeys === 'all') {
- return itemKeys as undefined | 'all';
+ return itemKeys as undefined | 'all' as TResult;
}
- return new Set([...itemKeys].map(String));
+ return new Set([...itemKeys].map(String)) as TResult;
}
diff --git a/packages/components/src/spectrum/utils/itemWrapperUtils.test.tsx b/packages/components/src/spectrum/utils/itemWrapperUtils.test.tsx
new file mode 100644
index 0000000000..7551074fd1
--- /dev/null
+++ b/packages/components/src/spectrum/utils/itemWrapperUtils.test.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { ItemContent } from '../ItemContent';
+import { Item } from '../shared';
+import { ITEM_EMPTY_STRING_TEXT_VALUE } from './itemUtils';
+import { wrapItemChildren } from './itemWrapperUtils';
+
+describe.each([null, { placement: 'top' }] as const)(
+ 'wrapItemChildren: %s',
+ tooltipOptions => {
+ it('should wrap primitives with Item elements', () => {
+ const given = [
+ 'Item 1',
+ 2,
+ 'Item 3',
+ '',
+ // eslint-disable-next-line react/jsx-key
+
- Item 4
,
+ -
+ Item 5
+
,
+ -
+ Item 6
+
,
+ ];
+
+ const expected = [
+ -
+ Item 1
+
,
+ -
+ 2
+
,
+ -
+ Item 3
+
,
+ -
+
+ {/* eslint-disable react/jsx-curly-brace-presence */}
+ {''}
+
+
,
+ -
+ Item 4
+
,
+ -
+ Item 5
+
,
+ -
+ Item 6
+
,
+ ];
+
+ const actual = wrapItemChildren(given, tooltipOptions);
+
+ expect(actual).toEqual(expected);
+ });
+ }
+);
diff --git a/packages/components/src/spectrum/utils/itemWrapperUtils.tsx b/packages/components/src/spectrum/utils/itemWrapperUtils.tsx
new file mode 100644
index 0000000000..47d8ae75bf
--- /dev/null
+++ b/packages/components/src/spectrum/utils/itemWrapperUtils.tsx
@@ -0,0 +1,91 @@
+import { cloneElement, ReactElement } from 'react';
+import { Item } from '@adobe/react-spectrum';
+import { isElementOfType } from '@deephaven/react-hooks';
+import {
+ isItemElement,
+ isSectionElement,
+ ItemElement,
+ ItemOrSection,
+ ITEM_EMPTY_STRING_TEXT_VALUE,
+ SectionElement,
+ TooltipOptions,
+} from './itemUtils';
+import { ItemProps } from '../shared';
+import ItemContent from '../ItemContent';
+
+/**
+ * Ensure all primitive children are wrapped in `Item` elements and that all
+ * `Item` element content is wrapped in `ItemContent` elements to handle text
+ * overflow consistently and to support tooltips.
+ * @param itemsOrSections The items or sections to wrap
+ * @param tooltipOptions The tooltip options to use when wrapping items
+ * @returns The wrapped items or sections
+ */
+export function wrapItemChildren(
+ itemsOrSections: ItemOrSection | ItemOrSection[],
+ tooltipOptions: TooltipOptions | null
+): (ItemElement | SectionElement)[] {
+ const itemsOrSectionsArray = Array.isArray(itemsOrSections)
+ ? itemsOrSections
+ : [itemsOrSections];
+
+ return itemsOrSectionsArray.map(item => {
+ if (isItemElement(item)) {
+ // Item content is already wrapped
+ if (isElementOfType(item.props.children, ItemContent)) {
+ return item;
+ }
+
+ const key = item.key ?? item.props.textValue;
+ const textValue =
+ item.props.textValue === ''
+ ? ITEM_EMPTY_STRING_TEXT_VALUE
+ : item.props.textValue;
+
+ // Wrap in `ItemContent` so we can support tooltips and handle text
+ // overflow
+ return cloneElement(item, {
+ ...item.props,
+ key,
+ textValue,
+ children: (
+
+ {item.props.children}
+
+ ),
+ });
+ }
+
+ if (isSectionElement(item)) {
+ return cloneElement(item, {
+ ...item.props,
+ key:
+ item.key ??
+ (typeof item.props.title === 'string' ? item.props.title : undefined),
+ children: wrapItemChildren(
+ item.props.children,
+ tooltipOptions
+ ) as ReactElement>[],
+ });
+ }
+
+ if (
+ typeof item === 'string' ||
+ typeof item === 'number' ||
+ typeof item === 'boolean'
+ ) {
+ const text = String(item);
+ const textValue = text === '' ? ITEM_EMPTY_STRING_TEXT_VALUE : text;
+
+ return (
+ -
+ {text}
+
+ );
+ }
+
+ return item;
+ });
+}
+
+export default wrapItemChildren;
diff --git a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx
index 2904bbb558..1f199653d7 100644
--- a/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx
+++ b/packages/components/src/spectrum/utils/useRenderNormalizedItem.tsx
@@ -1,7 +1,12 @@
import { Key, useCallback } from 'react';
import { ItemContent } from '../ItemContent';
import { Item } from '../shared';
-import { getItemKey, NormalizedItem, TooltipOptions } from './itemUtils';
+import {
+ getItemKey,
+ ITEM_EMPTY_STRING_TEXT_VALUE,
+ NormalizedItem,
+ TooltipOptions,
+} from './itemUtils';
/**
* Returns a render function that can be used to render a normalized item in
@@ -31,8 +36,10 @@ export function useRenderNormalizedItem(
// The `textValue` prop gets used to provide the content of `
diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts
index a429d9c1e7..8f5c4e89d4 100644
--- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts
+++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.test.ts
@@ -11,7 +11,7 @@ describe('useStringifiedMultiSelection', () => {
const normalizedItems: NormalizedItem[] = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(
i => ({
key: i,
- item: { key: i, content: `Item ${i}` },
+ item: { key: i, content: `Item ${i}`, textValue: `Item ${i}` },
})
);
diff --git a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts
index 04ec670168..843d847a92 100644
--- a/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts
+++ b/packages/components/src/spectrum/utils/useStringifiedMultiSelection.ts
@@ -71,8 +71,12 @@ export function useStringifiedMultiSelection({
const onStringSelectionChange = useCallback(
(keys: 'all' | Set) => {
+ if (onChange == null) {
+ return;
+ }
+
if (keys === 'all') {
- onChange?.('all');
+ onChange('all');
return;
}
@@ -84,7 +88,7 @@ export function useStringifiedMultiSelection({
}
});
- onChange?.(actualKeys);
+ onChange(actualKeys);
},
[normalizedItems, onChange]
);
diff --git a/packages/components/src/spectrum/utils/useStringifiedSelection.test.ts b/packages/components/src/spectrum/utils/useStringifiedSelection.test.ts
new file mode 100644
index 0000000000..3d442397bf
--- /dev/null
+++ b/packages/components/src/spectrum/utils/useStringifiedSelection.test.ts
@@ -0,0 +1,100 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { NormalizedItem } from './itemUtils';
+import { useStringifiedSelection } from './useStringifiedSelection';
+
+describe('useStringifiedSelection', () => {
+ const normalizedItems: NormalizedItem[] = [1, 2, 3, 4, 5, 6, 7, 8, 9].map(
+ i => ({
+ key: i,
+ item: { key: i, content: `Item ${i}`, textValue: `Item ${i}` },
+ })
+ );
+
+ const selectedKey = 1;
+ const defaultSelectedKey = 4;
+ const disabledKeys = [7, 8, 9];
+
+ const selectedStringKey = '1';
+ const defaultSelectedStringKey = '4';
+ const disabledStringKeys = new Set(['7', '8', '9']);
+
+ it.each([null, undefined])(
+ 'should return null or undefined for null or undefined keys: %s',
+ nullOrUndefinedKey => {
+ const { result } = renderHook(() =>
+ useStringifiedSelection({
+ normalizedItems,
+ selectedKey: nullOrUndefinedKey,
+ defaultSelectedKey: undefined,
+ disabledKeys: undefined,
+ onChange: undefined,
+ })
+ );
+
+ expect(result.current).toEqual({
+ selectedStringKey: nullOrUndefinedKey,
+ defaultSelectedStringKey: undefined,
+ disabledStringKeys: undefined,
+ onStringSelectionChange: expect.any(Function),
+ });
+ }
+ );
+
+ it('should stringify selections', () => {
+ const { result } = renderHook(() =>
+ useStringifiedSelection({
+ normalizedItems,
+ selectedKey,
+ defaultSelectedKey,
+ disabledKeys,
+ onChange: undefined,
+ })
+ );
+
+ expect(result.current).toEqual({
+ selectedStringKey,
+ defaultSelectedStringKey,
+ disabledStringKeys,
+ onStringSelectionChange: expect.any(Function),
+ });
+ });
+
+ it.each([undefined, jest.fn().mockName('onChange')])(
+ 'should call onChange with actual key: %s',
+ onChange => {
+ const { result } = renderHook(() =>
+ useStringifiedSelection({
+ normalizedItems,
+ selectedKey,
+ defaultSelectedKey,
+ disabledKeys,
+ onChange,
+ })
+ );
+
+ result.current.onStringSelectionChange('2');
+
+ if (onChange) {
+ expect(onChange).toHaveBeenCalledWith(2);
+ }
+ }
+ );
+
+ it('should call onChange with given key when actual key is not found', () => {
+ const onChange = jest.fn().mockName('onChange');
+
+ const { result } = renderHook(() =>
+ useStringifiedSelection({
+ normalizedItems,
+ selectedKey,
+ defaultSelectedKey,
+ disabledKeys,
+ onChange,
+ })
+ );
+
+ result.current.onStringSelectionChange('some.key');
+
+ expect(onChange).toHaveBeenCalledWith('some.key');
+ });
+});
diff --git a/packages/components/src/spectrum/utils/useStringifiedSelection.ts b/packages/components/src/spectrum/utils/useStringifiedSelection.ts
new file mode 100644
index 0000000000..ffea625f53
--- /dev/null
+++ b/packages/components/src/spectrum/utils/useStringifiedSelection.ts
@@ -0,0 +1,93 @@
+import { Key, useCallback, useMemo } from 'react';
+import {
+ getItemKey,
+ ItemKey,
+ itemSelectionToStringSet,
+ NormalizedItem,
+ NormalizedSection,
+} from './itemUtils';
+
+export interface UseStringifiedSelectionOptions {
+ normalizedItems: (NormalizedItem | NormalizedSection)[];
+ selectedKey: ItemKey | null | undefined;
+ defaultSelectedKey: ItemKey | undefined;
+ disabledKeys: Iterable | undefined;
+ onChange: ((key: ItemKey) => void) | undefined;
+}
+
+export interface UseStringifiedSelectionResult {
+ defaultSelectedStringKey?: Key;
+ selectedStringKey?: Key | null;
+ disabledStringKeys?: Set;
+ onStringSelectionChange: (key: Key) => void;
+}
+
+/**
+ * Spectrum collection components treat keys as strings if the `key` prop is
+ * explicitly set on `Item` elements. Since we do this in `useRenderNormalizedItem`,
+ * we need to ensure that keys are strings in order for selection to work. We
+ * then need to convert back to the original key types in the onChange handler.
+ * This hook encapsulates converting to and from strings so that keys can match
+ * the original key type.
+ * @param normalizedItems The normalized items to select from.
+ * @param selectedKey The currently selected key in the collection.
+ * @param defaultSelectedKey The initial selected key in the collection.
+ * @param disabledKeys The currently disabled keys in the collection.
+ * @param onChange Handler that is called when the selection changes.
+ * @returns UseStringifiedSelectionResult with stringified key sets and string
+ * key selection change handler.
+ */
+export function useStringifiedSelection({
+ normalizedItems,
+ defaultSelectedKey,
+ selectedKey,
+ disabledKeys,
+ onChange,
+}: UseStringifiedSelectionOptions): UseStringifiedSelectionResult {
+ const selectedStringKey = useMemo(
+ () => (selectedKey == null ? selectedKey : String(selectedKey)),
+ [selectedKey]
+ );
+
+ const defaultSelectedStringKey = useMemo(
+ () =>
+ defaultSelectedKey == null
+ ? defaultSelectedKey
+ : String(defaultSelectedKey),
+ [defaultSelectedKey]
+ );
+
+ const disabledStringKeys = useMemo(
+ () => itemSelectionToStringSet(disabledKeys),
+ [disabledKeys]
+ );
+
+ const onStringSelectionChange = useCallback(
+ (key: Key): void => {
+ if (onChange == null) {
+ return;
+ }
+
+ // The `key` arg will always be a string due to us setting the `Item` key
+ // prop in `renderItem`. We need to find the matching item to determine
+ // the actual key.
+ const selectedItem = normalizedItems.find(
+ item => String(getItemKey(item)) === key
+ );
+
+ const actualKey = getItemKey(selectedItem) ?? key;
+
+ onChange(actualKey);
+ },
+ [normalizedItems, onChange]
+ );
+
+ return {
+ selectedStringKey,
+ defaultSelectedStringKey,
+ disabledStringKeys,
+ onStringSelectionChange,
+ };
+}
+
+export default useStringifiedSelection;
diff --git a/packages/jsapi-components/src/spectrum/Picker.tsx b/packages/jsapi-components/src/spectrum/Picker.tsx
index 370c4b77cc..838aa08e7a 100644
--- a/packages/jsapi-components/src/spectrum/Picker.tsx
+++ b/packages/jsapi-components/src/spectrum/Picker.tsx
@@ -1,13 +1,18 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
import {
+ ItemKey,
+ NormalizedItem,
NormalizedItemData,
- Picker as PickerBase,
- PickerProps as PickerPropsBase,
+ NormalizedSection,
+ NormalizedSectionData,
+ PickerNormalized,
+ PickerProps as PickerBaseProps,
+ useSpectrumThemeProvider,
} from '@deephaven/components';
import { dh as DhType } from '@deephaven/jsapi-types';
import { Settings } from '@deephaven/jsapi-utils';
import Log from '@deephaven/log';
-import { PICKER_ITEM_HEIGHT, PICKER_TOP_OFFSET } from '@deephaven/utils';
-import { useCallback, useEffect, useMemo } from 'react';
+import { PICKER_ITEM_HEIGHTS, PICKER_TOP_OFFSET } from '@deephaven/utils';
import useFormatter from '../useFormatter';
import useGetItemIndexByValue from '../useGetItemIndexByValue';
import { useViewportData } from '../useViewportData';
@@ -16,7 +21,7 @@ import { useItemRowDeserializer } from './utils/useItemRowDeserializer';
const log = Log.module('jsapi-components.Picker');
-export interface PickerProps extends Omit {
+export interface PickerProps extends Omit {
table: DhType.Table;
/* The column of values to use as item keys. Defaults to the first column. */
keyColumn?: string;
@@ -32,12 +37,23 @@ export function Picker({
table,
keyColumn: keyColumnName,
labelColumn: labelColumnName,
- selectedKey,
settings,
+ onChange,
+ onSelectionChange,
...props
}: PickerProps): JSX.Element {
+ const { scale } = useSpectrumThemeProvider();
+ const itemHeight = PICKER_ITEM_HEIGHTS[scale];
+
const { getFormattedString: formatValue } = useFormatter(settings);
+ // `null` is a valid value for `selectedKey` in controlled mode, so we check
+ // for explicit `undefined` to identify uncontrolled mode.
+ const isUncontrolled = props.selectedKey === undefined;
+ const [uncontrolledSelectedKey, setUncontrolledSelectedKey] = useState(
+ props.defaultSelectedKey
+ );
+
const keyColumn = useMemo(
() => getItemKeyColumn(table, keyColumnName),
[keyColumnName, table]
@@ -53,7 +69,7 @@ export function Picker({
const getItemIndexByValue = useGetItemIndexByValue({
table,
columnName: keyColumn.name,
- value: selectedKey,
+ value: isUncontrolled ? uncontrolledSelectedKey : props.selectedKey,
});
const getInitialScrollPosition = useCallback(async () => {
@@ -63,19 +79,24 @@ export function Picker({
return null;
}
- return index * PICKER_ITEM_HEIGHT + PICKER_TOP_OFFSET;
- }, [getItemIndexByValue]);
+ return index * itemHeight + PICKER_TOP_OFFSET;
+ }, [getItemIndexByValue, itemHeight]);
const { viewportData, onScroll, setViewport } = useViewportData<
- NormalizedItemData,
+ NormalizedItemData | NormalizedSectionData,
DhType.Table
>({
reuseItemsOnTableResize: true,
table,
- itemHeight: PICKER_ITEM_HEIGHT,
+ itemHeight,
deserializeRow,
});
+ const normalizedItems = viewportData.items as (
+ | NormalizedItem
+ | NormalizedSection
+ )[];
+
useEffect(
// Set viewport to include the selected item so that its data will load and
// the real `key` will be available to show the selection in the UI.
@@ -101,16 +122,28 @@ export function Picker({
[getItemIndexByValue, settings, setViewport]
);
+ const onSelectionChangeInternal = useCallback(
+ (key: ItemKey): void => {
+ // If our component is uncontrolled, track the selected key internally
+ // so that we can scroll to the selected item if the user re-opens
+ if (isUncontrolled) {
+ setUncontrolledSelectedKey(key);
+ }
+
+ (onChange ?? onSelectionChange)?.(key);
+ },
+ [isUncontrolled, onChange, onSelectionChange]
+ );
+
return (
-
- {viewportData.items}
-
+ />
);
}
diff --git a/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.test.ts b/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.test.ts
index 6ae5cf95ff..1d0bff5fff 100644
--- a/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.test.ts
+++ b/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.test.ts
@@ -39,6 +39,7 @@ describe('useItemRowDeserializer', () => {
{
key: 'mock.keyValue',
content: 'mock.labelValue',
+ textValue: 'mock.labelValue',
},
],
[
@@ -49,6 +50,7 @@ describe('useItemRowDeserializer', () => {
{
key: 888,
content: '999',
+ textValue: '999',
},
],
[
@@ -59,6 +61,7 @@ describe('useItemRowDeserializer', () => {
{
key: true,
content: 'false',
+ textValue: 'false',
},
],
[
@@ -69,6 +72,7 @@ describe('useItemRowDeserializer', () => {
{
key: false,
content: 'true',
+ textValue: 'true',
},
],
[
@@ -79,6 +83,7 @@ describe('useItemRowDeserializer', () => {
{
key: String({}),
content: String({}),
+ textValue: String({}),
},
],
[
@@ -89,6 +94,7 @@ describe('useItemRowDeserializer', () => {
{
key: 'mock.keyValue',
content: formattedValue,
+ textValue: formattedValue,
},
],
])(
diff --git a/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.ts b/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.ts
index 6252209bd9..c09526d10e 100644
--- a/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.ts
+++ b/packages/jsapi-components/src/spectrum/utils/useItemRowDeserializer.ts
@@ -56,6 +56,7 @@ export function useItemRowDeserializer({
return {
key,
content,
+ textValue: content,
};
},
[formatValue, keyColumn, labelColumn]
diff --git a/packages/react-hooks/src/usePopoverOnScrollRef.ts b/packages/react-hooks/src/usePopoverOnScrollRef.ts
index 688eac9145..60d58501e8 100644
--- a/packages/react-hooks/src/usePopoverOnScrollRef.ts
+++ b/packages/react-hooks/src/usePopoverOnScrollRef.ts
@@ -21,7 +21,7 @@ export interface UsePopoverOnScrollRefResult {
export function usePopoverOnScrollRef(
findScrollArea: (ref: T | null) => HTMLElement | null,
onScroll: (event: Event) => void,
- getInitialScrollPosition?: () => Promise
+ getInitialScrollPosition?: () => Promise
): UsePopoverOnScrollRefResult {
const ref = useRef(null);
const isScrollOnOpenEnabledRef = useRef(false);
diff --git a/packages/utils/src/TextUtils.ts b/packages/utils/src/TextUtils.ts
index e75fbf63fc..fe372e9f3a 100644
--- a/packages/utils/src/TextUtils.ts
+++ b/packages/utils/src/TextUtils.ts
@@ -1,4 +1,6 @@
-class TextUtils {
+export const NON_BREAKING_SPACE = '\xa0';
+
+export class TextUtils {
/**
* Joins a list of strings with a comma, keeping the oxford comma and adding "and" as appropriate.
* Eg.
diff --git a/packages/utils/src/UIConstants.ts b/packages/utils/src/UIConstants.ts
index a7cf22fabf..2a5d7b962a 100644
--- a/packages/utils/src/UIConstants.ts
+++ b/packages/utils/src/UIConstants.ts
@@ -2,7 +2,11 @@ export const ACTION_ICON_HEIGHT = 24;
export const COMBO_BOX_ITEM_HEIGHT = 32;
export const COMBO_BOX_TOP_OFFSET = 4;
export const ITEM_KEY_PREFIX = 'DH_ITEM_KEY';
-export const PICKER_ITEM_HEIGHT = 32;
+// https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/listbox/src/ListBoxBase.tsx#L56
+export const PICKER_ITEM_HEIGHTS = {
+ medium: 32,
+ large: 48,
+} as const;
export const PICKER_TOP_OFFSET = 4;
export const TABLE_ROW_HEIGHT = 33;
export const SCROLL_DEBOUNCE_MS = 150;
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 0a1e112cb0..dc0afb0ada 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -18,7 +18,7 @@ export * from './ErrorUtils';
export * from './ObjectUtils';
export { default as RangeUtils, generateRange } from './RangeUtils';
export type { Range } from './RangeUtils';
-export { default as TextUtils } from './TextUtils';
+export * from './TextUtils';
export { default as TimeoutError } from './TimeoutError';
export { default as TimeUtils } from './TimeUtils';
export * from './TypeUtils';