+
Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/five-tools-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@crowdstrike/glide-core': patch
---

- Added an Options Group component for use in Menu. Use Options Group when want to group Option(s) semantically and visually.
- Option now has a minimum height of 28 pixels.
6 changes: 6 additions & 0 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ export default {
}

if (context.componentId === 'menu') {
for (const $option of $component.querySelectorAll(
'glide-core-options-group',
)) {
$option.removeAttribute('aria-label');
}

for (const $option of $component.querySelectorAll(
'glide-core-option',
)) {
Expand Down
144 changes: 144 additions & 0 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -5488,6 +5488,150 @@
}
]
},
{
"kind": "javascript-module",
"path": "src/options.group.styles.ts",
"declarations": [],
"exports": [
{
"kind": "js",
"name": "default",
"declaration": {
"module": "src/options.group.styles.ts"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/options.group.ts",
"declarations": [
{
"kind": "class",
"description": "",
"name": "OptionsGroup",
"slots": [
{
"name": "",
"description": "",
"type": {
"text": "Option"
}
}
],
"members": [
{
"kind": "field",
"name": "shadowRootOptions",
"type": {
"text": "ShadowRootInit"
},
"static": true,
"default": "{ ...LitElement.shadowRootOptions, mode: shadowRootMode, }"
},
{
"kind": "field",
"name": "label",
"type": {
"text": "string | undefined"
},
"default": "undefined",
"attribute": "label",
"reflects": true
},
{
"kind": "field",
"name": "hideLabel",
"type": {
"text": "boolean"
},
"default": "false",
"attribute": "hide-label",
"reflects": true
},
{
"kind": "field",
"name": "role",
"type": {
"text": "'group' | 'optgroup'"
},
"default": "'group'",
"attribute": "role",
"reflects": true
},
{
"kind": "field",
"name": "version",
"type": {
"text": "string"
},
"readonly": true,
"attribute": "version",
"reflects": true
}
],
"attributes": [
{
"name": "label",
"type": {
"text": "string | undefined"
},
"default": "undefined",
"fieldName": "label",
"required": true
},
{
"name": "hide-label",
"type": {
"text": "boolean"
},
"default": "false",
"fieldName": "hideLabel"
},
{
"name": "role",
"type": {
"text": "'group' | 'optgroup'"
},
"default": "'group'",
"fieldName": "role"
},
{
"name": "version",
"type": {
"text": "string"
},
"readonly": true,
"fieldName": "version"
}
],
"superclass": {
"name": "LitElement",
"package": "lit"
},
"tagName": "glide-core-options-group",
"customElement": true
}
],
"exports": [
{
"kind": "js",
"name": "default",
"declaration": {
"name": "OptionsGroup",
"module": "src/options.group.ts"
}
},
{
"kind": "custom-element-definition",
"name": "glide-core-options-group",
"declaration": {
"name": "OptionsGroup",
"module": "src/options.group.ts"
}
}
]
},
{
"kind": "javascript-module",
"path": "src/options.styles.ts",
Expand Down
83 changes: 83 additions & 0 deletions src/menu.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './button.js';
import './icons/storybook.js';
import './options.js';
import './options.group.js';
import type { Meta, StoryObj } from '@storybook/web-components';
import { html, nothing } from 'lit';
import { UPDATE_STORY_ARGS } from '@storybook/core-events';
Expand Down Expand Up @@ -45,6 +46,8 @@ const meta: Meta = {
version: '',
'<glide-core-options>[slot="default"]': '',
'<glide-core-options>.version': '',
'<glide-core-options-group>.label': 'A',
'<glide-core-options-group>.hide-label': false,
'<glide-core-option>.label': 'One',
'<glide-core-option>.addEventListener(event, handler)': '',
'<glide-core-option>.description': '',
Expand Down Expand Up @@ -166,6 +169,32 @@ const meta: Meta = {
type: { summary: 'string', detail: '// For debugging' },
},
},
'<glide-core-options-group>.label': {
name: 'label',
table: {
category: 'Options Group',
},
type: { name: 'string', required: true },
},
'<glide-core-options-group>.hide-label': {
name: 'hide-label',
table: {
category: 'Options Group',
defaultValue: { summary: 'false' },
type: { summary: 'boolean' },
},
},
'<glide-core-options-group>.version': {
control: false,
name: 'version',
table: {
category: 'Options Group',
defaultValue: {
summary: import.meta.env.VITE_GLIDE_CORE_VERSION,
},
type: { summary: 'string', detail: '// For debugging' },
},
},
'<glide-core-option>.label': {
name: 'label',
table: {
Expand Down Expand Up @@ -362,6 +391,60 @@ export default meta;

export const Menu: StoryObj = {};

export const WithGroups: StoryObj = {
render(arguments_) {
return html`<glide-core-menu
offset=${arguments_.offset === 4 ? nothing : arguments_.offset}
placement=${arguments_.placement === 'bottom-start'
? nothing
: arguments_.placement}
?loading=${arguments_.loading}
?open=${arguments_.open}
>
<glide-core-button label="Toggle" slot="target"></glide-core-button>

<glide-core-options>
<glide-core-options-group
label=${arguments_['<glide-core-options-group>.label']}
?hide-label=${arguments_['<glide-core-options-group>.hide-label']}
>
<glide-core-option
label=${arguments_['<glide-core-option>.label']}
description=${arguments_['<glide-core-option>.description'] ||
nothing}
value=${arguments_['<glide-core-option>.value'] || nothing}
?disabled=${arguments_['<glide-core-option>.disabled']}
></glide-core-option>

<glide-core-option label="Two"></glide-core-option>
<glide-core-option label="Three"></glide-core-option>
</glide-core-options-group>

<glide-core-options-group
label="B"
?hide-label=${arguments_['<glide-core-options-group>.hide-label']}
>
<glide-core-option label="Four"></glide-core-option>
<glide-core-option label="Five"></glide-core-option>
<glide-core-option label="Six"></glide-core-option>
</glide-core-options-group>

<glide-core-options-group
label="C"
?hide-label=${arguments_['<glide-core-options-group>.hide-label']}
>
<glide-core-option label="Seven"></glide-core-option>
<glide-core-option label="Eight"></glide-core-option>
<glide-core-option
label="Nine"
href=${arguments_['<glide-core-option>.href'] || nothing}
></glide-core-option>
</glide-core-options-group>
</glide-core-options>
</glide-core-menu>`;
},
};

export const WithIcons: StoryObj = {
args: {
'<glide-core-option>.label': 'Edit',
Expand Down
30 changes: 30 additions & 0 deletions src/menu.test.aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,36 @@ test('open=${false}', async ({ page }) => {
`);
});

test('<glide-core-options-group>', async ({ page }) => {
await page.goto('?id=menu--with-groups');

await page
.locator('glide-core-menu')
.first()
.evaluate<void, Menu>((element) => {
element.open = true;
});

await expect(page.locator('glide-core-menu')).toMatchAriaSnapshot(`
- button "Toggle"
- menu "Toggle":
- group "A":
- menuitem "One"
- menuitem "Two"
- menuitem "Three"
- group "B":
- menuitem "Four"
- menuitem "Five"
- menuitem "Six"
- group "C":
- menuitem "Seven"
- menuitem "Eight"
- menuitem "Nine":
- link "Nine":
- /url: /
`);
});

test('<glide-core-option>.description', async ({ page }) => {
await page.goto('?id=menu--menu');

Expand Down
49 changes: 49 additions & 0 deletions src/menu.test.interactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './options.js';
import './options.group.js';
import { LitElement } from 'lit';
import {
assert,
Expand Down Expand Up @@ -3143,6 +3144,54 @@ it('activates Options on hover when all or some are in a nested slot', async ()
).to.equal(options[2]?.id);
});

it('activates Options on hover when they are in Options Groups', async () => {
const host = await fixture<Menu>(html`
<glide-core-menu open>
<button slot="target">Target</button>

<glide-core-options>
<glide-core-options-group label="A">
<glide-core-option label="One"></glide-core-option>
<glide-core-option label="Two"></glide-core-option>
</glide-core-options-group>

<glide-core-options-group label="B">
<glide-core-option label="Three"></glide-core-option>
<glide-core-option label="Four"></glide-core-option>
</glide-core-options-group>
</glide-core-options>
</glide-core-menu>
`);

const options = host.querySelectorAll('glide-core-option');
await aTimeout(0); // Wait for Floating UI
await hover(options[1]);

expect(options[0]?.privateActive).to.be.false;
expect(options[1]?.privateActive).to.be.true;
expect(options[2]?.privateActive).to.be.false;
expect(options[3]?.privateActive).to.be.false;

expect(
host
.querySelector('glide-core-options')
?.getAttribute('aria-activedescendant'),
).to.equal(options[1]?.id);

await hover(options[3]);

expect(options[0]?.privateActive).to.be.false;
expect(options[1]?.privateActive).to.be.false;
expect(options[2]?.privateActive).to.be.false;
expect(options[3]?.privateActive).to.be.true;

expect(
host
.querySelector('glide-core-options')
?.getAttribute('aria-activedescendant'),
).to.equal(options[3]?.id);
});

it('activates the next enabled Option on ArrowDown', async () => {
const host = await fixture<Menu>(
html`<glide-core-menu open>
Expand Down
Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载