@jupyter-ai/persona-manager 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/persona-controls.d.ts +2 -2
- package/lib/persona-controls.js +14 -11
- package/package.json +4 -1
- package/src/__tests__/persona-controls-menus.spec.tsx +80 -97
- package/src/persona-controls.tsx +20 -15
- package/style/base.css +17 -1
|
@@ -56,7 +56,7 @@ export declare function applyControlChange(settings: PersonaSettings, control: C
|
|
|
56
56
|
* row is "Default" (selection = null); the rest are the persona's advertised
|
|
57
57
|
* options (selection = that option's id). Exported for tests.
|
|
58
58
|
*/
|
|
59
|
-
export declare function
|
|
59
|
+
export declare function ControlMenu(props: {
|
|
60
60
|
control: Control;
|
|
61
61
|
onSelect: (value: string | null) => void;
|
|
62
62
|
}): JSX.Element;
|
|
@@ -65,7 +65,7 @@ export declare function ControlItem(props: {
|
|
|
65
65
|
* menu (no nested dropdowns). Each control renders as a group label followed by
|
|
66
66
|
* its Default row and choices. Exported for tests.
|
|
67
67
|
*/
|
|
68
|
-
export declare function
|
|
68
|
+
export declare function OverflowControlsMenu(props: {
|
|
69
69
|
controls: Control[];
|
|
70
70
|
anchor: HTMLElement | null;
|
|
71
71
|
onClose: () => void;
|
package/lib/persona-controls.js
CHANGED
|
@@ -219,20 +219,23 @@ function defaultChoiceLabel(control) {
|
|
|
219
219
|
* The uppercase group label used in control menus: it titles a control's own
|
|
220
220
|
* dropdown and labels each control's section in the overflow menu. Rendered
|
|
221
221
|
* with MUI's `ListSubheader`, which has no tabindex, so arrow-key focus skips
|
|
222
|
-
* it and the menu stays keyboard-navigable.
|
|
222
|
+
* it and the menu stays keyboard-navigable. Sticky: when the menu scrolls, the
|
|
223
|
+
* label pins to the top, so the group the visible rows belong to stays
|
|
224
|
+
* readable; in the overflow menu the next section's label paints over it on
|
|
225
|
+
* arrival.
|
|
223
226
|
*/
|
|
224
|
-
function
|
|
225
|
-
return (React.createElement(ListSubheader, { id: props.id,
|
|
227
|
+
function ControlMenuSubheader(props) {
|
|
228
|
+
return (React.createElement(ListSubheader, { id: props.id, className: `${MENU_CLASS}-subheader` }, props.label));
|
|
226
229
|
}
|
|
227
230
|
// MUI's MenuList skips initial focus for children whose type carries this
|
|
228
231
|
// static; ListSubheader's own copy is hidden behind the wrapper.
|
|
229
|
-
|
|
232
|
+
ControlMenuSubheader.muiSkipListHighlight = true;
|
|
230
233
|
/**
|
|
231
234
|
* A dropdown for a control, titled with the control's label. The first choice
|
|
232
235
|
* row is "Default" (selection = null); the rest are the persona's advertised
|
|
233
236
|
* options (selection = that option's id). Exported for tests.
|
|
234
237
|
*/
|
|
235
|
-
export function
|
|
238
|
+
export function ControlMenu(props) {
|
|
236
239
|
const { control, onSelect } = props;
|
|
237
240
|
const [anchor, setAnchor] = useState(null);
|
|
238
241
|
// The heading names the menu for assistive tech: the subheader itself is a
|
|
@@ -243,7 +246,7 @@ export function ControlItem(props) {
|
|
|
243
246
|
React.createElement(Button, { className: `${SELECTOR_CLASS} ${SELECTOR_CLASS}-control-btn`, size: "small", variant: "text", disableRipple: true, endIcon: React.createElement(ArrowDropDownIcon, { className: `${SELECTOR_CLASS}-arrow` }), onClick: event => setAnchor(event.currentTarget), title: control.label },
|
|
244
247
|
React.createElement("span", { className: `${SELECTOR_CLASS}-control-value` }, currentControlLabel(control))),
|
|
245
248
|
React.createElement(Menu, { anchorEl: anchor, open: !!anchor, onClose: () => setAnchor(null), MenuListProps: { 'aria-labelledby': headingId }, ...menuAnchorProps },
|
|
246
|
-
React.createElement(
|
|
249
|
+
React.createElement(ControlMenuSubheader, { id: headingId, label: control.label }),
|
|
247
250
|
React.createElement(ChoiceMenuItem, { primary: defaultChoiceLabel(control), description: null, selected: control.selection === null, onSelect: () => {
|
|
248
251
|
setAnchor(null);
|
|
249
252
|
onSelect(null);
|
|
@@ -258,10 +261,10 @@ export function ControlItem(props) {
|
|
|
258
261
|
* menu (no nested dropdowns). Each control renders as a group label followed by
|
|
259
262
|
* its Default row and choices. Exported for tests.
|
|
260
263
|
*/
|
|
261
|
-
export function
|
|
264
|
+
export function OverflowControlsMenu(props) {
|
|
262
265
|
const { controls, anchor, onClose, onChange } = props;
|
|
263
266
|
return (React.createElement(Menu, { anchorEl: anchor, open: !!anchor, onClose: onClose, MenuListProps: { 'aria-label': 'More controls' }, ...menuAnchorProps }, controls.flatMap(control => [
|
|
264
|
-
React.createElement(
|
|
267
|
+
React.createElement(ControlMenuSubheader, { key: `${control.id}-label`, label: control.label }),
|
|
265
268
|
React.createElement(ChoiceMenuItem, { key: `${control.id}-default`, primary: defaultChoiceLabel(control), description: null, selected: control.selection === null, onSelect: () => {
|
|
266
269
|
onClose();
|
|
267
270
|
onChange(control, null);
|
|
@@ -340,12 +343,12 @@ function ControlsRow(props) {
|
|
|
340
343
|
const visible = controls.slice(0, visibleCount);
|
|
341
344
|
const overflow = controls.slice(visibleCount);
|
|
342
345
|
return (React.createElement("div", { className: `${SELECTOR_CLASS}-controls`, ref: rowRef },
|
|
343
|
-
React.createElement("div", { className: `${SELECTOR_CLASS}-controls-measure`, ref: measureRef, "aria-hidden": "true" }, controls.map(control => (React.createElement(
|
|
344
|
-
visible.map(control => (React.createElement(
|
|
346
|
+
React.createElement("div", { className: `${SELECTOR_CLASS}-controls-measure`, ref: measureRef, "aria-hidden": "true" }, controls.map(control => (React.createElement(ControlMenu, { key: control.id, control: control, onSelect: v => onChange(control, v) })))),
|
|
347
|
+
visible.map(control => (React.createElement(ControlMenu, { key: control.id, control: control, onSelect: v => onChange(control, v) }))),
|
|
345
348
|
overflow.length ? (React.createElement(React.Fragment, null,
|
|
346
349
|
React.createElement("button", { type: "button", ref: overflowBtnRef, className: `${SELECTOR_CLASS} ${SELECTOR_CLASS}-overflow-btn`, onClick: event => setOverflowAnchor(event.currentTarget), title: "More controls", "aria-label": "More controls" },
|
|
347
350
|
React.createElement(MoreHorizIcon, { fontSize: "small" })),
|
|
348
|
-
React.createElement(
|
|
351
|
+
React.createElement(OverflowControlsMenu, { controls: overflow, anchor: overflowAnchor, onClose: () => setOverflowAnchor(null), onChange: onChange }))) : null));
|
|
349
352
|
}
|
|
350
353
|
// All formatters pin the `en` locale so numbers agree with each other and
|
|
351
354
|
// with the surrounding English labels.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter-ai/persona-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The core manager & registry for AI personas in Jupyter AI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@jupyterlab/builder": "^4.0.0",
|
|
69
69
|
"@jupyterlab/testutils": "^4.0.0",
|
|
70
|
+
"@testing-library/dom": "^10.4.0",
|
|
71
|
+
"@testing-library/react": "^16.3.0",
|
|
72
|
+
"@testing-library/user-event": "^14.6.0",
|
|
70
73
|
"@types/jest": "^29.2.0",
|
|
71
74
|
"@types/json-schema": "^7.0.11",
|
|
72
75
|
"@types/react": "^18.0.26",
|
|
@@ -5,16 +5,13 @@
|
|
|
5
5
|
* type-ahead, and Enter drive the choice rows.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
17
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import {
|
|
11
|
+
Control,
|
|
12
|
+
ControlMenu,
|
|
13
|
+
OverflowControlsMenu
|
|
14
|
+
} from '../persona-controls';
|
|
18
15
|
|
|
19
16
|
const SUBHEADER_SELECTOR = 'li.jp-jai-controlMenu-subheader';
|
|
20
17
|
|
|
@@ -32,105 +29,84 @@ function modelControl(selection: string | null): Control {
|
|
|
32
29
|
};
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
container = document.createElement('div');
|
|
41
|
-
document.body.appendChild(container);
|
|
42
|
-
root = createRoot(container);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
act(() => root.unmount());
|
|
47
|
-
container.remove();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
function menu(): HTMLElement {
|
|
51
|
-
return document.querySelector('ul[role="menu"]') as HTMLElement;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function focused(): HTMLElement {
|
|
55
|
-
return document.activeElement as HTMLElement;
|
|
56
|
-
}
|
|
32
|
+
function heading(): HTMLElement {
|
|
33
|
+
return document.querySelector(SUBHEADER_SELECTOR) as HTMLElement;
|
|
34
|
+
}
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
36
|
+
function focused(): HTMLElement {
|
|
37
|
+
return document.activeElement as HTMLElement;
|
|
38
|
+
}
|
|
65
39
|
|
|
40
|
+
describe('control menus', () => {
|
|
66
41
|
describe('control dropdown', () => {
|
|
67
|
-
function openMenu(
|
|
42
|
+
async function openMenu(
|
|
68
43
|
control: Control,
|
|
69
44
|
onSelect: (value: string | null) => void = () => undefined
|
|
70
|
-
):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(container.querySelector('button') as HTMLElement).dispatchEvent(
|
|
76
|
-
new MouseEvent('click', { bubbles: true })
|
|
77
|
-
);
|
|
78
|
-
});
|
|
45
|
+
): Promise<ReturnType<typeof userEvent.setup>> {
|
|
46
|
+
const user = userEvent.setup();
|
|
47
|
+
render(<ControlMenu control={control} onSelect={onSelect} />);
|
|
48
|
+
await user.click(screen.getByRole('button'));
|
|
49
|
+
return user;
|
|
79
50
|
}
|
|
80
51
|
|
|
81
|
-
it('opens titled by a heading that names the menu', () => {
|
|
82
|
-
openMenu(modelControl('beta'));
|
|
83
|
-
|
|
84
|
-
expect(heading
|
|
85
|
-
expect(heading
|
|
86
|
-
expect(
|
|
87
|
-
|
|
52
|
+
it('opens titled by a heading that names the menu', async () => {
|
|
53
|
+
await openMenu(modelControl('beta'));
|
|
54
|
+
expect(heading().textContent).toBe('Model');
|
|
55
|
+
expect(heading().hasAttribute('tabindex')).toBe(false);
|
|
56
|
+
expect(heading().id).toBeTruthy();
|
|
57
|
+
expect(screen.getByRole('menu').getAttribute('aria-labelledby')).toBe(
|
|
58
|
+
heading().id
|
|
59
|
+
);
|
|
60
|
+
// Sticky, so the title stays visible while a long option list scrolls.
|
|
61
|
+
expect(heading().classList.contains('MuiListSubheader-sticky')).toBe(
|
|
62
|
+
true
|
|
63
|
+
);
|
|
88
64
|
});
|
|
89
65
|
|
|
90
|
-
it('focuses the selected row and moves focus with the keyboard', () => {
|
|
91
|
-
openMenu(modelControl('beta'));
|
|
66
|
+
it('focuses the selected row and moves focus with the keyboard', async () => {
|
|
67
|
+
const user = await openMenu(modelControl('beta'));
|
|
92
68
|
// Rows: heading, "Default (Alpha)", "Alpha", "Beta"; "Beta" is selected.
|
|
93
|
-
expect(focused()
|
|
94
|
-
expect(focused().getAttribute('role')).toBe('menuitem');
|
|
69
|
+
expect(focused()).toBe(screen.getByRole('menuitem', { name: 'Beta' }));
|
|
95
70
|
// Wrapping from the last row skips the heading to the first choice row.
|
|
96
|
-
|
|
97
|
-
expect(focused()
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
await user.keyboard('{ArrowDown}');
|
|
72
|
+
expect(focused()).toBe(
|
|
73
|
+
screen.getByRole('menuitem', { name: 'Default (Alpha)' })
|
|
74
|
+
);
|
|
75
|
+
await user.keyboard('{ArrowDown}');
|
|
76
|
+
expect(focused()).toBe(screen.getByRole('menuitem', { name: 'Alpha' }));
|
|
100
77
|
// Type-ahead: "b" jumps to Beta.
|
|
101
|
-
|
|
102
|
-
expect(focused()
|
|
78
|
+
await user.keyboard('b');
|
|
79
|
+
expect(focused()).toBe(screen.getByRole('menuitem', { name: 'Beta' }));
|
|
103
80
|
});
|
|
104
81
|
|
|
105
|
-
it('leaves focus in place when type-ahead matches only the heading', () => {
|
|
106
|
-
openMenu(modelControl('beta'));
|
|
82
|
+
it('leaves focus in place when type-ahead matches only the heading', async () => {
|
|
83
|
+
const user = await openMenu(modelControl('beta'));
|
|
107
84
|
// First key after open, so it cannot buffer onto a previous press: "m"
|
|
108
85
|
// matches the heading ("Model") and no choice row, and the heading
|
|
109
86
|
// never takes focus.
|
|
110
|
-
|
|
111
|
-
expect(focused()
|
|
87
|
+
await user.keyboard('m');
|
|
88
|
+
expect(focused()).toBe(screen.getByRole('menuitem', { name: 'Beta' }));
|
|
112
89
|
});
|
|
113
90
|
|
|
114
|
-
it('falls back to the first choice row when the selection is stale', () => {
|
|
91
|
+
it('falls back to the first choice row when the selection is stale', async () => {
|
|
115
92
|
// A selection id the persona no longer advertises leaves no row
|
|
116
93
|
// selected; initial focus then skips the heading to the first row.
|
|
117
|
-
openMenu(modelControl('stale'));
|
|
118
|
-
expect(focused()
|
|
119
|
-
|
|
94
|
+
await openMenu(modelControl('stale'));
|
|
95
|
+
expect(focused()).toBe(
|
|
96
|
+
screen.getByRole('menuitem', { name: 'Default (Alpha)' })
|
|
97
|
+
);
|
|
120
98
|
});
|
|
121
99
|
|
|
122
|
-
it('activates the focused row with Enter', () => {
|
|
100
|
+
it('activates the focused row with Enter', async () => {
|
|
123
101
|
const onSelect = jest.fn();
|
|
124
|
-
openMenu(modelControl(null), onSelect);
|
|
125
|
-
|
|
126
|
-
press('ArrowDown');
|
|
127
|
-
press('Enter');
|
|
102
|
+
const user = await openMenu(modelControl(null), onSelect);
|
|
103
|
+
await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
|
|
128
104
|
expect(onSelect).toHaveBeenCalledWith('beta');
|
|
129
105
|
});
|
|
130
106
|
});
|
|
131
107
|
|
|
132
108
|
describe('overflow menu', () => {
|
|
133
|
-
it('is labeled and arrows skip the section headings', () => {
|
|
109
|
+
it('is labeled and arrows skip the section headings', async () => {
|
|
134
110
|
const first: Control = {
|
|
135
111
|
id: 'a',
|
|
136
112
|
kind: 'setting',
|
|
@@ -143,32 +119,39 @@ describe('control menus', () => {
|
|
|
143
119
|
id: 'b',
|
|
144
120
|
kind: 'setting',
|
|
145
121
|
label: 'Bbb',
|
|
146
|
-
current:
|
|
122
|
+
current: 'b1',
|
|
147
123
|
selection: 'stale',
|
|
148
124
|
options: [{ id: 'b1', name: 'B one', description: null }]
|
|
149
125
|
};
|
|
126
|
+
const user = userEvent.setup();
|
|
150
127
|
const anchor = document.createElement('button');
|
|
151
128
|
document.body.appendChild(anchor);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
129
|
+
render(
|
|
130
|
+
<OverflowControlsMenu
|
|
131
|
+
controls={[first, second]}
|
|
132
|
+
anchor={anchor}
|
|
133
|
+
onClose={() => undefined}
|
|
134
|
+
onChange={() => undefined}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
expect(screen.getByRole('menu').getAttribute('aria-label')).toBe(
|
|
138
|
+
'More controls'
|
|
139
|
+
);
|
|
163
140
|
const headings = Array.from(
|
|
164
141
|
document.querySelectorAll(SUBHEADER_SELECTOR)
|
|
165
|
-
)
|
|
166
|
-
expect(headings).toEqual(['Aaa', 'Bbb']);
|
|
142
|
+
);
|
|
143
|
+
expect(headings.map(h => h.textContent)).toEqual(['Aaa', 'Bbb']);
|
|
144
|
+
// Sticky, so the section a scrolled row belongs to stays visible.
|
|
145
|
+
for (const h of headings) {
|
|
146
|
+
expect(h.classList.contains('MuiListSubheader-sticky')).toBe(true);
|
|
147
|
+
}
|
|
167
148
|
// "A one" is the only selected row; ArrowDown crosses the "Bbb" heading
|
|
168
149
|
// to the next section's first choice row.
|
|
169
|
-
expect(focused()
|
|
170
|
-
|
|
171
|
-
expect(focused()
|
|
150
|
+
expect(focused()).toBe(screen.getByRole('menuitem', { name: 'A one' }));
|
|
151
|
+
await user.keyboard('{ArrowDown}');
|
|
152
|
+
expect(focused()).toBe(
|
|
153
|
+
screen.getByRole('menuitem', { name: 'Default (B one)' })
|
|
154
|
+
);
|
|
172
155
|
anchor.remove();
|
|
173
156
|
});
|
|
174
157
|
});
|
package/src/persona-controls.tsx
CHANGED
|
@@ -333,15 +333,17 @@ function defaultChoiceLabel(control: Control): string {
|
|
|
333
333
|
* The uppercase group label used in control menus: it titles a control's own
|
|
334
334
|
* dropdown and labels each control's section in the overflow menu. Rendered
|
|
335
335
|
* with MUI's `ListSubheader`, which has no tabindex, so arrow-key focus skips
|
|
336
|
-
* it and the menu stays keyboard-navigable.
|
|
336
|
+
* it and the menu stays keyboard-navigable. Sticky: when the menu scrolls, the
|
|
337
|
+
* label pins to the top, so the group the visible rows belong to stays
|
|
338
|
+
* readable; in the overflow menu the next section's label paints over it on
|
|
339
|
+
* arrival.
|
|
337
340
|
*/
|
|
338
|
-
function
|
|
341
|
+
function ControlMenuSubheader(props: {
|
|
342
|
+
label: string;
|
|
343
|
+
id?: string;
|
|
344
|
+
}): JSX.Element {
|
|
339
345
|
return (
|
|
340
|
-
<ListSubheader
|
|
341
|
-
id={props.id}
|
|
342
|
-
disableSticky
|
|
343
|
-
className={`${MENU_CLASS}-subheader`}
|
|
344
|
-
>
|
|
346
|
+
<ListSubheader id={props.id} className={`${MENU_CLASS}-subheader`}>
|
|
345
347
|
{props.label}
|
|
346
348
|
</ListSubheader>
|
|
347
349
|
);
|
|
@@ -349,14 +351,14 @@ function ControlSubheader(props: { label: string; id?: string }): JSX.Element {
|
|
|
349
351
|
|
|
350
352
|
// MUI's MenuList skips initial focus for children whose type carries this
|
|
351
353
|
// static; ListSubheader's own copy is hidden behind the wrapper.
|
|
352
|
-
|
|
354
|
+
ControlMenuSubheader.muiSkipListHighlight = true;
|
|
353
355
|
|
|
354
356
|
/**
|
|
355
357
|
* A dropdown for a control, titled with the control's label. The first choice
|
|
356
358
|
* row is "Default" (selection = null); the rest are the persona's advertised
|
|
357
359
|
* options (selection = that option's id). Exported for tests.
|
|
358
360
|
*/
|
|
359
|
-
export function
|
|
361
|
+
export function ControlMenu(props: {
|
|
360
362
|
control: Control;
|
|
361
363
|
onSelect: (value: string | null) => void;
|
|
362
364
|
}): JSX.Element {
|
|
@@ -388,7 +390,7 @@ export function ControlItem(props: {
|
|
|
388
390
|
MenuListProps={{ 'aria-labelledby': headingId }}
|
|
389
391
|
{...menuAnchorProps}
|
|
390
392
|
>
|
|
391
|
-
<
|
|
393
|
+
<ControlMenuSubheader id={headingId} label={control.label} />
|
|
392
394
|
<ChoiceMenuItem
|
|
393
395
|
primary={defaultChoiceLabel(control)}
|
|
394
396
|
description={null}
|
|
@@ -420,7 +422,7 @@ export function ControlItem(props: {
|
|
|
420
422
|
* menu (no nested dropdowns). Each control renders as a group label followed by
|
|
421
423
|
* its Default row and choices. Exported for tests.
|
|
422
424
|
*/
|
|
423
|
-
export function
|
|
425
|
+
export function OverflowControlsMenu(props: {
|
|
424
426
|
controls: Control[];
|
|
425
427
|
anchor: HTMLElement | null;
|
|
426
428
|
onClose: () => void;
|
|
@@ -436,7 +438,10 @@ export function OverflowMenu(props: {
|
|
|
436
438
|
{...menuAnchorProps}
|
|
437
439
|
>
|
|
438
440
|
{controls.flatMap(control => [
|
|
439
|
-
<
|
|
441
|
+
<ControlMenuSubheader
|
|
442
|
+
key={`${control.id}-label`}
|
|
443
|
+
label={control.label}
|
|
444
|
+
/>,
|
|
440
445
|
<ChoiceMenuItem
|
|
441
446
|
key={`${control.id}-default`}
|
|
442
447
|
primary={defaultChoiceLabel(control)}
|
|
@@ -550,7 +555,7 @@ function ControlsRow(props: {
|
|
|
550
555
|
aria-hidden="true"
|
|
551
556
|
>
|
|
552
557
|
{controls.map(control => (
|
|
553
|
-
<
|
|
558
|
+
<ControlMenu
|
|
554
559
|
key={control.id}
|
|
555
560
|
control={control}
|
|
556
561
|
onSelect={v => onChange(control, v)}
|
|
@@ -559,7 +564,7 @@ function ControlsRow(props: {
|
|
|
559
564
|
</div>
|
|
560
565
|
|
|
561
566
|
{visible.map(control => (
|
|
562
|
-
<
|
|
567
|
+
<ControlMenu
|
|
563
568
|
key={control.id}
|
|
564
569
|
control={control}
|
|
565
570
|
onSelect={v => onChange(control, v)}
|
|
@@ -578,7 +583,7 @@ function ControlsRow(props: {
|
|
|
578
583
|
>
|
|
579
584
|
<MoreHorizIcon fontSize="small" />
|
|
580
585
|
</button>
|
|
581
|
-
<
|
|
586
|
+
<OverflowControlsMenu
|
|
582
587
|
controls={overflow}
|
|
583
588
|
anchor={overflowAnchor}
|
|
584
589
|
onClose={() => setOverflowAnchor(null)}
|
package/style/base.css
CHANGED
|
@@ -229,6 +229,15 @@
|
|
|
229
229
|
box-shadow: var(--jp-elevation-z6);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/* Keep focus-scrolled rows clear of the stuck group label: when arrow keys
|
|
233
|
+
scroll a row into view at the top, offset it by the label's height so the
|
|
234
|
+
label does not cover it. Only menus containing a group label need the
|
|
235
|
+
offset; the persona picker and usage popover share this paper class and
|
|
236
|
+
keep edge-aligned scrolling. */
|
|
237
|
+
.jp-jai-controlMenu-paper:has(.jp-jai-controlMenu-subheader) {
|
|
238
|
+
scroll-padding-top: 32px;
|
|
239
|
+
}
|
|
240
|
+
|
|
232
241
|
/* stylelint-disable selector-class-pattern -- theming MUI's menu item classes */
|
|
233
242
|
.jp-jai-controlMenu-paper .MuiMenuItem-root {
|
|
234
243
|
padding: 6px 12px;
|
|
@@ -281,7 +290,11 @@
|
|
|
281
290
|
/* Control menu group label: an uppercase heading above a control's choices.
|
|
282
291
|
It titles each control's own dropdown; in the overflow popover a hairline
|
|
283
292
|
divider and top spacing separate the controls' sections. Scoped under the
|
|
284
|
-
paper to win over MUI's ListSubheader defaults.
|
|
293
|
+
paper to win over MUI's ListSubheader defaults. The labels are sticky:
|
|
294
|
+
sticky is MUI's ListSubheader default, and its sticky class supplies the
|
|
295
|
+
positioning. The opaque background is load-bearing, scrolled rows pass
|
|
296
|
+
beneath it. One line only, so stacked sticky labels in the overflow menu
|
|
297
|
+
cover each other exactly. */
|
|
285
298
|
.jp-jai-controlMenu-paper .jp-jai-controlMenu-subheader {
|
|
286
299
|
margin-top: 8px;
|
|
287
300
|
padding: 8px 12px 2px;
|
|
@@ -294,6 +307,9 @@
|
|
|
294
307
|
text-transform: uppercase;
|
|
295
308
|
color: var(--jp-ui-font-color1);
|
|
296
309
|
background-color: var(--jp-layout-color1);
|
|
310
|
+
white-space: nowrap;
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
text-overflow: ellipsis;
|
|
297
313
|
}
|
|
298
314
|
|
|
299
315
|
/* The first (or only) label sits flush at the top, no divider or extra gap
|