Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/short-pans-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@spectrum-web-components/status-light': patch
---

**Fixed**: Added missing `accent` and `cyan` variant to status light.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ parameters:
# 3. Commit this change to the PR branch where the changes exist.
current_golden_images_hash:
type: string
default: eb24194a08a84696ddd7eb582d08118302b95ed7
default: 71be19cab16d3ff600820e801341f14629ad06d9
wireit_cache_name:
type: string
default: wireit
Expand Down
7 changes: 3 additions & 4 deletions .cursor/rules/github-description.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ Use the following labels to categorize pull requests. Only use labels that exist
- `Browser: Edge (Legacy)`: Issue with pre-chromium Edge
- `Browser: FireFox`: Firefox browser issues
- `Browser: Safari`: Safari browser issues
- `iOS`: iOS-specific issues and bugs

### Development and process labels

Expand All @@ -107,11 +108,9 @@ Common additional labels include:

- `chore`: Routine tasks, maintenance, or non-feature changes
- `dependencies`: Updates or changes to project dependencies
- `docs`: Documentation updates or improvements
- `enhancement`: Improvements to existing features
- `feature`: New feature implementations
- `Documentation`: Documentation updates or improvements
- `feature`: New feature implementations or improvements to existing features
- `i18n`: Internationalization and localization work
- `iOS`: iOS-specific issues and bugs
- `mobile`: Mobile platform issues and responsive design
- `performance`: Performance-related improvements or regressions
- `refactor`: Code restructuring and refactoring work
Expand Down
2 changes: 1 addition & 1 deletion .cursor/rules/jira-ticket.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Use the following labels to categorize tickets appropriately:
- `s2foundations`: Spectrum 2 Foundations related work
- `spectrum2`: Spectrum 2 platform specific tasks
- `team-processes`: Internal team workflow improvements
- `testing`: Test implementation or testing infrastructure work
- `test`: Test implementation or testing infrastructure work
- `triage`: New tickets requiring team assessment and prioritization
- `VoiceOver`: VoiceOver screen reader specific issues

Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Feature request
description: Describe the feature you would like added
title: '[Feat]: '
labels: [enhancement, triage, needs jira ticket]
labels: [feature, triage, needs jira ticket]
# assignees:
body:
- type: markdown
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/new_component.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: New component
description: Outline the requirements for a new component
title: '[NEW]: '
labels: [missing components, triage, needs jira ticket]
labels: [new component, triage, needs jira ticket]
# assignees:
body:
- type: markdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ selects.args = {
open: true,
};
selects.decorators = [isOverlayOpen];
selects.swc_vrt = {
skip: true,
};

export const iconOnly = (args: StoryArgs = {}): TemplateResult =>
Template(args);
Expand Down
102 changes: 81 additions & 21 deletions first-gen/packages/overlay/src/HoverController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,32 @@ import {
lastInteractionType,
} from './InteractionController.js';

const HOVER_DELAY = 300;

export class HoverController extends InteractionController {
override type = InteractionTypes.hover;

private elementIds: string[] = [];

focusedin = false;
private targetFocused = false;

private hoverTimeout?: ReturnType<typeof setTimeout>;

pointerentered = false;
private hovering = false;

private overlayFocused = false;

handleKeyup(event: KeyboardEvent): void {
if (event.code === 'Tab' || event.code === 'Escape') {
if (event.code === 'Tab') {
this.open = true;
} else if (event.code === 'Escape') {
if (this.open) {
event.preventDefault();
event.stopPropagation();
this.open = false;
// Return focus to trigger element
if (this.target) {
this.target.focus();
}
}
}
}

Expand All @@ -52,23 +62,29 @@ export class HoverController extends InteractionController {
}

this.open = true;
this.focusedin = true;
this.targetFocused = true;
}

handleTargetFocusout(): void {
this.focusedin = false;
if (this.pointerentered) return;
this.open = false;
this.targetFocused = false;
// Don't close immediately if pointer is over the content
if (this.hovering) return;
// Use delay to allow focus to move into overlay content
this.doFocusleave();
}

handleTargetPointerenter(): void {
private clearCloseTimeout(): void {
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = undefined;
}
}

handleTargetPointerenter(): void {
this.clearCloseTimeout();
if (this.overlay?.disabled) return;
this.open = true;
this.pointerentered = true;
this.hovering = true;
}

handleTargetPointerleave(): void {
Expand All @@ -78,16 +94,28 @@ export class HoverController extends InteractionController {
// set a timeout once the pointer enters and the overlay is shown
// give the user time to enter the overlay
handleHostPointerenter(): void {
if (this.hoverTimeout) {
clearTimeout(this.hoverTimeout);
this.hoverTimeout = undefined;
}
this.clearCloseTimeout();
}

handleHostPointerleave(): void {
this.doPointerleave();
}

handleOverlayFocusin(): void {
this.overlayFocused = true;
// Clear any pending close timeout when focus enters overlay
this.clearCloseTimeout();
}

handleOverlayFocusout(): void {
this.overlayFocused = false;
// Don't close immediately if pointer is over the content or trigger has focus
if (this.hovering) return;
if (this.targetFocused && this.target.matches(':focus-visible')) return;
// Use delay before closing
this.doFocusleave();
}

override prepareDescription(): void {
// require "content" to apply relationship
if (!this.overlay.elements.length) return;
Expand Down Expand Up @@ -138,14 +166,31 @@ export class HoverController extends InteractionController {
};
}

protected doPointerleave(): void {
this.pointerentered = false;
const triggerElement = this.target as HTMLElement;
if (this.focusedin && triggerElement.matches(':focus-visible')) return;

private scheduleClose(): void {
this.hoverTimeout = setTimeout(() => {
this.open = false;
}, HOVER_DELAY);
}, 300);
}

private doPointerleave(): void {
this.hovering = false;
const triggerElement = this.target as HTMLElement;
if (this.targetFocused && triggerElement.matches(':focus-visible'))
return;
// Don't close if focus is within overlay content
if (this.overlayFocused) return;

this.scheduleClose();
}

private doFocusleave(): void {
// Clear any existing timeout
this.clearCloseTimeout();

// Use same delay as pointer interactions for consistency
if (!this.targetFocused && !this.overlayFocused && !this.hovering) {
this.scheduleClose();
}
}

override init(): void {
Expand Down Expand Up @@ -198,5 +243,20 @@ export class HoverController extends InteractionController {
() => this.handleHostPointerleave(),
{ signal }
);
this.overlay.addEventListener(
'focusin',
() => this.handleOverlayFocusin(),
{ signal }
);
this.overlay.addEventListener(
'focusout',
() => this.handleOverlayFocusout(),
{ signal }
);
this.overlay.addEventListener(
'keyup',
(event) => this.handleKeyup(event),
{ signal }
);
}
}
87 changes: 87 additions & 0 deletions first-gen/packages/overlay/stories/overlay.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import '@spectrum-web-components/dialog/sp-dialog.js';
import '@spectrum-web-components/field-label/sp-field-label.js';
import '@spectrum-web-components/icons-workflow/icons/sp-icon-magnify.js';
import '@spectrum-web-components/icons-workflow/icons/sp-icon-open-in.js';
import '@spectrum-web-components/link/sp-link.js';
import {
openOverlay,
Overlay,
Expand Down Expand Up @@ -1654,6 +1655,92 @@ export const triggeredByOptimization = (): TemplateResult => {
`;
};

export const hoverWithInteractiveContent = (): TemplateResult => {
return html`
<div
style="display: flex; gap: 20px; flex-direction: column; padding: 40px;"
>
<!-- Hover with interactive buttons -->
<overlay-trigger triggered-by="hover" placement="right">
<sp-button slot="trigger">
Hover for interactive buttons
</sp-button>
<sp-popover slot="hover-content" tip>
<sp-dialog size="s" no-divider>
<h3 style="margin-top: 0;">Interactive content</h3>
<p>Tab into these buttons:</p>
<div
style="display: flex; gap: 8px; flex-direction: column;"
>
<sp-button>Action 1</sp-button>
<sp-button>Action 2</sp-button>
<sp-button>Action 3</sp-button>
</div>
</sp-dialog>
</sp-popover>
</overlay-trigger>

<!-- Hover with links -->
<overlay-trigger triggered-by="hover" placement="right">
<sp-button slot="trigger">
Hover for interactive links
</sp-button>
<sp-popover slot="hover-content" tip>
<sp-dialog size="s" no-divider>
<h3 style="margin-top: 0;">Quick links</h3>
<ul>
<li>
<sp-link href="#example1">
Example link 1
</sp-link>
</li>
<li>
<sp-link href="#example2">
Example link 2
</sp-link>
</li>
<li>
<sp-link href="#example3">
Example link 3
</sp-link>
</li>
</ul>
</sp-dialog>
</sp-popover>
</overlay-trigger>

<!-- Hover with action group (like Arrange icon example) -->
<overlay-trigger triggered-by="hover" placement="right">
<sp-button slot="trigger">Hover for action group</sp-button>
<sp-popover slot="hover-content" tip>
<sp-action-group
selects="single"
vertical
style="margin: var(--spectrum-spacing-200);"
>
<sp-action-button>
<sp-icon-magnify slot="icon"></sp-icon-magnify>
Send to Front
</sp-action-button>
<sp-action-button>
<sp-icon-magnify slot="icon"></sp-icon-magnify>
Send to Back
</sp-action-button>
<sp-action-button>
<sp-icon-magnify slot="icon"></sp-icon-magnify>
Align Center
</sp-action-button>
</sp-action-group>
</sp-popover>
</overlay-trigger>
</div>
`;
};

hoverWithInteractiveContent.swc_vrt = {
skip: true,
};

export const pickerInDialog = (): TemplateResult => {
return html`
<sp-button variant="primary" id="mybutton">Button popover</sp-button>
Expand Down
Loading
Loading