Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select list option groups #2111

Merged
merged 69 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
be2d3f4
Initial implementation with unit tests. Need storybook and matrix tests.
atmgrifter00 May 14, 2024
137659c
Add Storybook configuration. Fix style.
atmgrifter00 May 14, 2024
be83a30
Change files
atmgrifter00 May 15, 2024
5356a9e
Style ordering.
atmgrifter00 May 15, 2024
f13c4dc
Matrix tests
atmgrifter00 May 15, 2024
00c129d
Code cleanup. Matrix tests.
atmgrifter00 May 15, 2024
e800b06
Doc update.
atmgrifter00 May 15, 2024
b6e58bc
Merge from main.
atmgrifter00 May 15, 2024
8fc0d64
Merge branch 'main' into list-option-groups
atmgrifter00 May 15, 2024
8e68687
Doc updates
atmgrifter00 May 15, 2024
df054e5
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 May 15, 2024
43e765c
Add test.
atmgrifter00 May 15, 2024
6204776
Merge branch 'main' into list-option-groups
atmgrifter00 May 15, 2024
4f1b555
Putting tests back in.
atmgrifter00 May 16, 2024
12d89f1
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 May 16, 2024
0d88074
Incremental code cleanup. Likely not done.
atmgrifter00 May 16, 2024
eabd142
A lttle cleanup.
atmgrifter00 May 16, 2024
d2df0d0
More cleanup.
atmgrifter00 May 16, 2024
5e1be6f
Code cleanup and fixes.
atmgrifter00 May 17, 2024
3cac707
Prettier
atmgrifter00 May 17, 2024
460473d
Remove unneeded function.
atmgrifter00 May 17, 2024
22e4c7d
Minor clean up
atmgrifter00 May 17, 2024
57608e5
Handling PR feedback.
atmgrifter00 May 17, 2024
092a797
Merge branch 'main' into list-option-groups
atmgrifter00 May 17, 2024
48ccc64
Linting
atmgrifter00 May 17, 2024
78eea82
Update spec.
atmgrifter00 May 20, 2024
ce24440
Prettier.
atmgrifter00 May 21, 2024
17c6fd6
Merge branch 'main' into list-option-groups
atmgrifter00 May 21, 2024
08d9177
Update packages/storybook/src/nimble/select/select-opened-matrix.stor…
atmgrifter00 May 21, 2024
f500ce5
Update packages/storybook/src/nimble/select/select-opened-matrix.stor…
atmgrifter00 May 21, 2024
4e6499c
Add some tests.
atmgrifter00 May 21, 2024
d0dd6de
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 May 21, 2024
ff1519e
Handle PR feedback.
atmgrifter00 May 22, 2024
cf619b1
Handle PR feedback.
atmgrifter00 May 23, 2024
1c11ef3
Adding test.
atmgrifter00 May 23, 2024
02376f9
Refactor and handle PR feedback.
atmgrifter00 May 24, 2024
8e35701
Some member renames.
atmgrifter00 May 24, 2024
9979569
Fix tests.
atmgrifter00 May 24, 2024
3f6d3d0
Small fix and adding some tests.
atmgrifter00 May 24, 2024
28444b4
Fix Combobox tests.
atmgrifter00 May 24, 2024
ac7e908
Merge branch 'main' into list-option-groups
atmgrifter00 May 24, 2024
f3a2800
Handling more cases and adding more tests.
atmgrifter00 May 24, 2024
303bc38
Update packages/nimble-components/src/list-option-group/tests/list-op…
atmgrifter00 May 30, 2024
63bc230
Handle PR feedback.
atmgrifter00 May 30, 2024
c39122a
Merge branch 'main' into list-option-groups
atmgrifter00 May 30, 2024
4de7d6f
Styling fix. Add matrix test.
atmgrifter00 May 30, 2024
a6ae0c2
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 May 30, 2024
7e228eb
Handle PR feedback.
atmgrifter00 May 31, 2024
76942e5
Update packages/storybook/src/nimble/select/select.stories.ts
atmgrifter00 May 31, 2024
5ed5734
Missed an update.
atmgrifter00 May 31, 2024
6c84d82
Fix tests.
atmgrifter00 May 31, 2024
ec47661
Merge branch 'main' into list-option-groups
atmgrifter00 May 31, 2024
b458c82
Fix template.
atmgrifter00 May 31, 2024
edc5111
One minor reversion.
atmgrifter00 May 31, 2024
e3d0828
Merge branch 'main' into list-option-groups
atmgrifter00 May 31, 2024
04f4083
Merge branch 'main' into list-option-groups
atmgrifter00 May 31, 2024
1e2cae3
Merge branch 'main' into list-option-groups
atmgrifter00 May 31, 2024
04089e1
Merge branch 'main' into list-option-groups
atmgrifter00 Jun 3, 2024
d9e33f7
Handle PR feedback.
atmgrifter00 Jun 3, 2024
0a8c069
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 Jun 3, 2024
5f7d6d1
Missed some changes.
atmgrifter00 Jun 3, 2024
e2df031
Fix compile.
atmgrifter00 Jun 3, 2024
defda31
Some code cleanup.
atmgrifter00 Jun 3, 2024
5efa298
Update packages/nimble-components/src/list-option-group/tests/list-op…
atmgrifter00 Jun 3, 2024
4b087fc
Merge branch 'main' into list-option-groups
atmgrifter00 Jun 3, 2024
c4aa27a
Handle PR feedback.
atmgrifter00 Jun 3, 2024
57ea04f
Merge branch 'list-option-groups' of https://github.com/ni/nimble int…
atmgrifter00 Jun 3, 2024
48078b1
Merge branch 'main' into list-option-groups
atmgrifter00 Jun 3, 2024
66da400
Prettier.
atmgrifter00 Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor",
"comment": "List option groups for Select",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/nimble-components/src/all-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import './icons/all-icons';
import './label-provider/core';
import './label-provider/table';
import './list-option';
import './list-option-group';
import './mapping/empty';
import './mapping/icon';
import './mapping/spinner';
Expand Down
100 changes: 100 additions & 0 deletions packages/nimble-components/src/list-option-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation';
import { observable, attr, volatile } from '@microsoft/fast-element';
import { styles } from './styles';
import { template } from './template';
import { ListOption } from '../list-option';

declare global {
interface HTMLElementTagNameMap {
'nimble-list-option-group': ListOptionGroup;
}
}

/**
* A nimble-styled HTML listbox option
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
*/
export class ListOptionGroup extends FoundationElement {
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
/**
* The label for the group.
*
* @public
* @remarks
* If a label is also provided via slotted content, the label attribute
* will have precedence.
*/
@attr
public label?: string;

/**
* The hidden state of the element.
*
* @public
* @defaultValue - false
* @remarks
* HTML Attribute: hidden
*/
@attr({ mode: 'boolean' })
public override hidden = false;
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal
* This attribute is required to allow use-cases that offer dynamic filtering
* (like the Select) to visually hide groups that are filtered out, but still
* allow users to use the native 'hidden' attribute without it being affected
* by the filtering process.
*/
@attr({ attribute: 'visually-hidden', mode: 'boolean' })
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
public visuallyHidden = false;

/** @internal */
@observable
public hasOverflow = false;

/** @internal */
public labelSlot!: HTMLSlotElement;

/** @internal */
@observable
public slottedElements: Element[] = [];

/** @internal */
@volatile
public get labelContent(): string {
if (this.label || !this.$fastController.isConnected) {
return this.label ?? '';
}
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved

const nodes = this.labelSlot.assignedNodes();
return nodes.length > 0
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
? nodes.map(node => node.textContent?.trim()).join(' ')
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
: '';
}

/**
* @internal
*/
public clickHandler(e: MouseEvent): void {
e.preventDefault();
e.stopImmediatePropagation();
}

protected slottedElementsChanged(): void {
this.slottedElements.forEach(e => {
if (e instanceof ListOption) {
e.slot = 'options-slot';
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
}
});
}
}

const nimbleListOptionGroup = ListOptionGroup.compose({
baseName: 'list-option-group',
baseClass: FoundationElement,
template,
styles
});

DesignSystem.getOrCreate()
.withPrefix('nimble')
.register(nimbleListOptionGroup());
export const listOptionGroupTag = 'nimble-list-option-group';
38 changes: 38 additions & 0 deletions packages/nimble-components/src/list-option-group/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { css } from '@microsoft/fast-element';
import { display } from '@microsoft/fast-foundation';

import {
groupHeaderFont,
groupHeaderFontColor,
groupHeaderTextTransform,
smallPadding
} from '../theme-provider/design-tokens';

export const styles = css`
${display('flex')}

:host {
cursor: default;
justify-content: left;
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
flex-direction: column;
}

:host([visually-hidden]) {
display: none;
}

.header {
font: ${groupHeaderFont};
text-transform: ${groupHeaderTextTransform};
color: ${groupHeaderFontColor};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: ${smallPadding};
margin-bottom: ${smallPadding};
}

.label-slot {
display: none;
}
`;
38 changes: 38 additions & 0 deletions packages/nimble-components/src/list-option-group/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { html, ref, slotted } from '@microsoft/fast-element';
import { isListboxOption } from '@microsoft/fast-foundation';
import type { ListOptionGroup } from '.';
import { overflow } from '../utilities/directive/overflow';

const slottedContentFilter = (n: Node): boolean => {
const allowed = n instanceof HTMLElement && (isListboxOption(n) || n instanceof Text);
return allowed;
};

// prettier-ignore
export const template = html<ListOptionGroup>`
<template
role="group"
aria-label="${x => x.labelContent}"
>
<span ${overflow('hasOverflow')}
class="header"
aria-hidden="true"
title=${x => (x.hasOverflow && x.labelContent ? x.labelContent : null)}
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
>
${x => x.labelContent}
<slot ${ref('labelSlot')}
class="label-slot"
${slotted({
filter: (n: Node) => slottedContentFilter(n),
flatten: true,
property: 'slottedElements',
})}
>
</slot>
</span>
<span class="content" part="content" role="none">
<slot name="options-slot"></slot>
</span>
</template>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { html } from '@microsoft/fast-element';
import { ListOptionGroup, listOptionGroupTag } from '..';
import { fixture, type Fixture } from '../../utilities/tests/fixture';
import { waitForUpdatesAsync } from '../../testing/async-helpers';

describe('ListboxOptionGroup', () => {
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
async function setup(): Promise<Fixture<ListOptionGroup>> {
return fixture<ListOptionGroup>(
html`<nimble-list-option-group style="width: 200px" label="Group 1">
</nimble-list-option-group>`
);
}

const getGroupLabelDisplay = (element: ListOptionGroup): string => {
return (
element.shadowRoot!.querySelector('.header')!.textContent?.trim()
?? ''
);
};

it('should export its tag', () => {
expect(listOptionGroupTag).toBe('nimble-list-option-group');
});

it('can construct an element instance', () => {
expect(
document.createElement('nimble-list-option-group')
).toBeInstanceOf(ListOptionGroup);
});

it('if label attribute and slotted label are provided, label attribute is displayed', async () => {
const { element, connect, disconnect } = await setup();
await connect();
const slottedLabel = document.createElement('span');
slottedLabel.textContent = 'Slotted Label';
element.appendChild(slottedLabel);
await waitForUpdatesAsync();

expect(getGroupLabelDisplay(element)).toBe('Group 1');

await disconnect();
});

it('if label attribute is not provided, slotted label is displayed', async () => {
const { element, connect, disconnect } = await setup();
await connect();
const slottedLabel = document.createElement('span');
slottedLabel.textContent = 'Slotted Label';
element.label = undefined;
element.appendChild(slottedLabel);
await waitForUpdatesAsync();

expect(getGroupLabelDisplay(element)).toBe('Slotted Label');
await disconnect();
});

it('if multiple slotted labels are provided, all are appended to the labelContent', async () => {
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
atmgrifter00 marked this conversation as resolved.
Show resolved Hide resolved
const { element, connect, disconnect } = await setup();
await connect();
element.label = undefined;
const slottedLabel1 = document.createElement('span');
slottedLabel1.textContent = 'Slotted Label 1';
element.appendChild(slottedLabel1);
const slottedLabel2 = document.createElement('span');
slottedLabel2.textContent = 'Slotted Label 2';
element.appendChild(slottedLabel2);
await waitForUpdatesAsync();

expect(getGroupLabelDisplay(element)).toBe(
'Slotted Label 1 Slotted Label 2'
);
await disconnect();
});

describe('title overflow', () => {
let element: ListOptionGroup;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;

function dispatchEventToListOptionGroup(
event: Event
): boolean | undefined {
return element
.shadowRoot!.querySelector('.header')!
.dispatchEvent(event);
}

function getListOptionGroupTitle(): string {
return (
element
.shadowRoot!.querySelector('.header')!
.getAttribute('title') ?? ''
);
}

beforeEach(async () => {
({ element, connect, disconnect } = await setup());
await connect();
});

afterEach(async () => {
await disconnect();
});

it('sets title when option text is ellipsized', async () => {
const optionContent = 'a very long value that should get ellipsized due to not fitting within the allocated width';
element.label = optionContent;
await waitForUpdatesAsync();
dispatchEventToListOptionGroup(new MouseEvent('mouseover'));
await waitForUpdatesAsync();
expect(getListOptionGroupTitle()).toBe(optionContent);
});

it('does not set title when option text is fully visible', async () => {
const optionContent = 'short value';
element.label = optionContent;
dispatchEventToListOptionGroup(new MouseEvent('mouseover'));
await waitForUpdatesAsync();
expect(getListOptionGroupTitle()).toBe('');
});

it('removes title on mouseout of option', async () => {
const optionContent = 'a very long value that should get ellipsized due to not fitting within the allocated width';
element.label = optionContent;
dispatchEventToListOptionGroup(new MouseEvent('mouseover'));
await waitForUpdatesAsync();
dispatchEventToListOptionGroup(new MouseEvent('mouseout'));
await waitForUpdatesAsync();
expect(getListOptionGroupTitle()).toBe('');
});
});
});
Loading
Loading