Skip to content

Commit 8661726

Browse files
authored
Add listbox component (#65)
* Add listbox * Custom style for listbox focus * Add reference snapshots
1 parent 58d1a7d commit 8661726

17 files changed

+1452
-17
lines changed

packages/components/docs/Introduction.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Here is the list of components part of the Jupyter UI toolkit:
2222
| `data-grid` | [Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) | [Stories](?path=/story/components-data-grid--documentation) |
2323
| `date-field` | [Date input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) | [Stories](?path=/story/components-date-field--documentation) |
2424
| `divider` | [Horizontal or vertical rule](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr) | [Stories](?path=/story/components-divider--documentation) |
25+
| `listbox` | [Listbox](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) | [Stories](?path=/story/components-listbox--documentation) |
2526
| `menu` | [Menu](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) | [Stories](?path=/story/components-menu--documentation) |
2627
| `number-field` | [Number input element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number) | [Stories](?path=/story/components-number-field--documentation) |
2728
| `progress` | [Meter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/meter/) as line | [Stories](?path=/story/components-progress--documentation) |

packages/components/docs/api-report.md

Lines changed: 1197 additions & 17 deletions
Large diffs are not rendered by default.

packages/components/src/custom-elements.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { jpCombobox } from './combobox/index';
1717
import { jpDataGrid, jpDataGridCell, jpDataGridRow } from './data-grid/index';
1818
import { jpDateField } from './date-field/index';
1919
import { jpDivider } from './divider/index';
20+
import { jpListbox } from './listbox/index';
2021
import { jpMenu } from './menu/index';
2122
import { jpMenuItem } from './menu-item/index';
2223
import { jpNumberField } from './number-field/index';
@@ -58,6 +59,7 @@ import type { Combobox } from './combobox/index';
5859
import type { DataGrid, DataGridCell, DataGridRow } from './data-grid/index';
5960
import type { DateField } from './date-field/index';
6061
import type { Divider } from './divider/index';
62+
import type { ListboxElement } from './listbox/index';
6163
import type { Menu } from './menu/index';
6264
import type { MenuItem } from './menu-item/index';
6365
import type { NumberField } from './number-field/index';
@@ -100,6 +102,7 @@ export {
100102
jpDataGridRow,
101103
jpDateField,
102104
jpDivider,
105+
jpListbox,
103106
jpMenu,
104107
jpMenuItem,
105108
jpNumberField,
@@ -149,6 +152,7 @@ export const allComponents = {
149152
jpDataGridRow,
150153
jpDateField,
151154
jpDivider,
155+
jpListbox,
152156
jpMenu,
153157
jpMenuItem,
154158
jpNumberField,

packages/components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export * from './combobox/index';
2424
export * from './date-field/index';
2525
export * from './data-grid/index';
2626
export * from './divider/index';
27+
export * from './listbox/index';
2728
export * from './menu/index';
2829
export * from './menu-item/index';
2930
export * from './number-field/index';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import {
5+
ListboxElement,
6+
listboxTemplate as template
7+
} from '@microsoft/fast-foundation';
8+
import { listboxStyles as styles } from './listbox.styles';
9+
10+
/**
11+
* The Jupyter listbox Custom Element. Implements, {@link @microsoft/fast-foundation#Listbox}
12+
* {@link @microsoft/fast-foundation#ListboxTemplate}
13+
*
14+
*
15+
* @public
16+
* @remarks
17+
* HTML Element: \<jp-listbox\>
18+
*
19+
*/
20+
export const jpListbox = ListboxElement.compose({
21+
baseName: 'listbox',
22+
template,
23+
styles
24+
});
25+
26+
/**
27+
* Base class for ListBox
28+
* @public
29+
*/
30+
export { ListboxElement };
31+
32+
export { styles as listboxStyles };
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import type { StoryFn, Meta, StoryObj } from '@storybook/html';
5+
import { setTheme } from '../utilities/storybook';
6+
7+
export default {
8+
title: 'Components/Listbox',
9+
argTypes: {
10+
isDisabled: { control: 'boolean' },
11+
multiple: { control: 'boolean' }
12+
},
13+
parameters: {
14+
actions: {
15+
disabled: true
16+
}
17+
}
18+
} as Meta;
19+
20+
const Template: StoryFn = (args, context): string => {
21+
const {
22+
globals: { backgrounds, accent },
23+
parameters
24+
} = context;
25+
setTheme(accent, parameters.backgrounds, backgrounds);
26+
27+
return `<jp-listbox
28+
${args.isDisabled ? 'disabled' : ''}
29+
${args.multiple ? 'multiple' : ''}
30+
>
31+
<jp-option value="1">Option 1</jp-option>
32+
<jp-option value="2">Option 2</jp-option>
33+
<jp-option value="3">Option 3</jp-option>
34+
</jp-listbox>`;
35+
};
36+
37+
export const Default: StoryObj = { render: Template.bind({}) };
38+
Default.args = {
39+
isDisabled: false,
40+
multiple: false
41+
};
42+
43+
export const WithDisabled: StoryObj = { render: Template.bind({}) };
44+
WithDisabled.args = {
45+
...Default.args,
46+
isDisabled: true
47+
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Copyright (c) Microsoft Corporation.
3+
// Distributed under the terms of the Modified BSD License.
4+
5+
import type { ElementStyles } from '@microsoft/fast-element';
6+
import { css } from '@microsoft/fast-element';
7+
import type { FoundationElementTemplate } from '@microsoft/fast-foundation';
8+
import {
9+
disabledCursor,
10+
display,
11+
focusVisible,
12+
forcedColorsStylesheetBehavior,
13+
ListboxElement,
14+
ListboxOption
15+
} from '@microsoft/fast-foundation';
16+
import { SystemColors } from '@microsoft/fast-web-utilities';
17+
import {
18+
controlCornerRadius,
19+
designUnit,
20+
disabledOpacity,
21+
fillColor,
22+
focusStrokeOuter,
23+
focusStrokeWidth,
24+
neutralStrokeRest,
25+
strokeWidth
26+
} from '@microsoft/fast-components';
27+
import { heightNumber } from '../styles/size';
28+
29+
/**
30+
* Styles for Listbox
31+
* @public
32+
*/
33+
export const listboxStyles: FoundationElementTemplate<ElementStyles> = (
34+
context,
35+
definition
36+
) => {
37+
const ListboxOptionTag = context.tagFor(ListboxOption);
38+
const hostContext =
39+
context.name === context.tagFor(ListboxElement) ? '' : '.listbox';
40+
41+
// The expression interpolations present in this block cause Prettier to generate
42+
// various formatting bugs.
43+
// prettier-ignore
44+
return css`
45+
${!hostContext ? display('inline-flex') : ''}
46+
47+
:host ${hostContext} {
48+
background: ${fillColor};
49+
border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest};
50+
border-radius: calc(${controlCornerRadius} * 1px);
51+
box-sizing: border-box;
52+
flex-direction: column;
53+
padding: calc(${designUnit} * 1px) 0;
54+
}
55+
56+
${!hostContext ? css`
57+
:host(:${focusVisible}:not([disabled])) {
58+
outline: none;
59+
}
60+
61+
:host(:focus-within:not([disabled])) {
62+
border-color: ${focusStrokeOuter};
63+
box-shadow: 0 0 0
64+
calc((${focusStrokeWidth} - ${strokeWidth}) * 1px)
65+
${focusStrokeOuter} inset;
66+
}
67+
68+
:host([disabled]) ::slotted(*) {
69+
cursor: ${disabledCursor};
70+
opacity: ${disabledOpacity};
71+
pointer-events: none;
72+
}
73+
` : ''}
74+
75+
${hostContext || ':host([size])'} {
76+
max-height: calc(
77+
(var(--size) * ${heightNumber} + (${designUnit} * ${strokeWidth} * 2)) * 1px
78+
);
79+
overflow-y: auto;
80+
}
81+
82+
:host([size="0"]) ${hostContext} {
83+
max-height: none;
84+
}
85+
`.withBehaviors(
86+
forcedColorsStylesheetBehavior(
87+
css`
88+
:host(:not([multiple]):${focusVisible}) ::slotted(${ListboxOptionTag}[aria-selected="true"]),
89+
:host([multiple]:${focusVisible}) ::slotted(${ListboxOptionTag}[aria-checked="true"]) {
90+
border-color: ${SystemColors.ButtonText};
91+
box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) inset ${SystemColors.HighlightText};
92+
}
93+
94+
:host(:not([multiple]):${focusVisible}) ::slotted(${ListboxOptionTag}[aria-selected="true"]) {
95+
background: ${SystemColors.Highlight};
96+
color: ${SystemColors.HighlightText};
97+
fill: currentcolor;
98+
}
99+
100+
::slotted(${ListboxOptionTag}[aria-selected="true"]:not([aria-checked="true"])) {
101+
background: ${SystemColors.Highlight};
102+
border-color: ${SystemColors.HighlightText};
103+
color: ${SystemColors.HighlightText};
104+
}
105+
`
106+
)
107+
);
108+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { test, expect } from '@playwright/test';
5+
6+
test('Default', async ({ page }) => {
7+
await page.goto('/iframe.html?id=components-listbox--default');
8+
9+
expect(await page.locator('jp-listbox').screenshot()).toMatchSnapshot(
10+
'listbox-default.png'
11+
);
12+
});
13+
14+
test('Disabled', async ({ page }) => {
15+
await page.goto('/iframe.html?id=components-listbox--with-disabled');
16+
17+
expect(await page.locator('jp-listbox').screenshot()).toMatchSnapshot(
18+
'listbox-disabled.png'
19+
);
20+
});

packages/lab-example/src/index.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
DataGrid,
1717
DateField,
1818
Divider,
19+
Listbox,
1920
Menu,
2021
MenuItem,
2122
NumberField,
@@ -146,6 +147,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
146147
const select = widget.node.querySelector('jp-select');
147148
select?.addEventListener('change', changeListener('Select'));
148149

150+
const listbox = widget.node.querySelector('jp-listbox');
151+
listbox?.addEventListener('change', changeListener('Listbox'));
152+
149153
const combobox = widget.node.querySelector('jp-combobox');
150154
combobox?.addEventListener('change', changeListener('Combobox'));
151155
combobox?.addEventListener('input', changeConsoleListener('Combobox'));
@@ -282,6 +286,14 @@ function Artwork(props: { dataRef: React.Ref<WebDataGrid> }): JSX.Element {
282286
<Option>Option Label #3</Option>
283287
</Select>
284288
</div>
289+
<div className="jp-FlexColumn">
290+
<label>Listbox</label>
291+
<Listbox onChange={onChange}>
292+
<Option>Option Label #1</Option>
293+
<Option>Option Label #2</Option>
294+
<Option>Option Label #3</Option>
295+
</Listbox>
296+
</div>
285297
<div className="jp-FlexColumn">
286298
<label>Combobox</label>
287299
<Combobox onChange={onChange} onInput={onChangeConsole}>
@@ -477,6 +489,16 @@ function createNode(): HTMLElement {
477489
<jp-option>Option Label #3</jp-option>
478490
</jp-select>
479491
</div>
492+
<div class="jp-FlexColumn">
493+
<label>
494+
Listbox
495+
</label>
496+
<jp-listbox>
497+
<jp-option>Option Label #1</jp-option>
498+
<jp-option>Option Label #2</jp-option>
499+
<jp-option>Option Label #3</jp-option>
500+
</jp-listbox>
501+
</div>
480502
<div class="jp-FlexColumn">
481503
<label>
482504
Combobox

packages/react-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * from './combobox';
1616
export * from './data-grid';
1717
export * from './date-field';
1818
export * from './divider';
19+
export * from './listbox';
1920
export * from './menu';
2021
export * from './menu-item';
2122
export * from './number-field';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { provideJupyterDesignSystem, jpListbox } from '@jupyter/web-components';
5+
import { provideReactWrapper } from '@microsoft/fast-react-wrapper';
6+
import React from 'react';
7+
8+
const { wrap } = provideReactWrapper(React, provideJupyterDesignSystem());
9+
10+
export const Listbox: React.DetailedHTMLFactory<
11+
React.HTMLAttributes<HTMLElement> & {
12+
disabled?: boolean;
13+
multiple?: boolean;
14+
size?: number;
15+
},
16+
HTMLElement
17+
> = wrap(jpListbox()) as any;
18+
// @ts-expect-error unknown property
19+
Listbox.displayName = 'Jupyter.Listbox';

0 commit comments

Comments
 (0)