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

feat: improved command selector with scroll and scroll into view #118

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
14 changes: 12 additions & 2 deletions example/dev.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
import open from 'open';
await open('./dist/index.html');
// Dynamic import handling
const openFile = async () => {
try {
// Use dynamic import to load the ES module
const open = (await import('open')).default;
await open('./dist/index.html');
} catch (err) {
console.error('Error opening file:', err);
}
};

openFile();
25 changes: 22 additions & 3 deletions src/components/chat-item/chat-prompt-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@ export class ChatPromptInput {
});

this.quickPickOpen = true;

const titleElements = document.querySelectorAll('.mynah-chat-command-selector-group-title');
if (titleElements.length > 0) {
const observer = new IntersectionObserver(
([ e ]) => e.target.classList.toggle('stuck', e.intersectionRatio < 1),
{ threshold: [ 1 ] }
);

titleElements.forEach((element) => observer.observe(element));
}
}
}
} else {
Expand Down Expand Up @@ -335,11 +345,20 @@ export class ChatPromptInput {
}

if (nextElementIndex !== -1) {
// Remove the active class from the previously selected command
commandElements[lastActiveElement]?.classList.remove('target-command');
commandElements[nextElementIndex].classList.add('target-command');
if (commandElements[nextElementIndex].getAttribute('prompt') !== null) {
this.promptTextInput.updateTextInputValue(commandElements[nextElementIndex].getAttribute('prompt') as string);

// Add the active class to the new selected command
const selectedElement = commandElements[nextElementIndex];
selectedElement.classList.add('target-command');

// Update the input value with the selected command
if (selectedElement.getAttribute('prompt') !== null) {
this.promptTextInput.updateTextInputValue(selectedElement.getAttribute('prompt') as string);
}

// Ensure the selected command is scrolled into view
selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else {
if (this.quickPick != null) {
Expand Down
80 changes: 47 additions & 33 deletions src/components/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,25 @@ export class Overlay {
private readonly innerContainer: ExtendedHTMLElement;
private readonly guid = generateUID();
private readonly onClose;
horizontalDirection: OverlayHorizontalDirection;
verticalDirection: OverlayVerticalDirection;
referenceElement: HTMLElement | ExtendedHTMLElement | undefined;
referencePoint: { top: number; left: number } | undefined;
stretchWidth: boolean;

constructor (props: OverlayProps) {
const horizontalDirection = props.horizontalDirection ?? OverlayHorizontalDirection.TO_RIGHT;
const verticalDirection = props.verticalDirection ?? OverlayVerticalDirection.START_TO_BOTTOM;
this.horizontalDirection = props.horizontalDirection ?? OverlayHorizontalDirection.TO_RIGHT;
this.verticalDirection = props.verticalDirection ?? OverlayVerticalDirection.START_TO_BOTTOM;
this.referenceElement = props.referenceElement;
this.referencePoint = props.referencePoint;
this.stretchWidth = props.stretchWidth ?? false;
this.onClose = props.onClose;
const dimOutside = props.dimOutside !== false;
const closeOnOutsideClick = props.closeOnOutsideClick !== false;

const calculatedTop = this.getCalculatedTop(verticalDirection, props.referenceElement, props.referencePoint);
const calculatedLeft = this.getCalculatedLeft(horizontalDirection, props.referenceElement, props.referencePoint);
const calculatedWidth = props.stretchWidth === true ? this.getCalculatedWidth(props.referenceElement) : 0;
const calculatedTop = this.getCalculatedTop();
const calculatedLeft = this.getCalculatedLeft();
const calculatedWidth = this.stretchWidth ? this.getCalculatedWidth() : 0;

this.innerContainer = DomBuilder.getInstance().build({
type: 'div',
Expand All @@ -102,7 +110,7 @@ export class Overlay {

this.container = DomBuilder.getInstance().build({
type: 'div',
classNames: [ 'mynah-overlay-container', horizontalDirection, verticalDirection, props.background !== false ? 'background' : '' ],
classNames: [ 'mynah-overlay-container', this.horizontalDirection, this.verticalDirection, props.background !== false ? 'background' : '' ],
attributes: {
style: `top: ${calculatedTop}px; left: ${calculatedLeft}px; ${calculatedWidth !== 0 ? `width: ${calculatedWidth}px;` : ''}`,
},
Expand Down Expand Up @@ -159,19 +167,35 @@ export class Overlay {
this.container.style.left = `${effectiveLeft - (lastContainerRect.left + lastContainerRect.width + OVERLAY_MARGIN - winWidth)}px`;
}

window.addEventListener('resize', this.calculatePosition);

// we need to delay the class toggle
// to avoid the skipping of the transition comes from css
// for a known js-css relation problem
setTimeout(() => {
this.render.addClass('mynah-overlay-open');

this.calculatePosition();
if (closeOnOutsideClick) {
window.addEventListener('blur', this.windowBlurHandler.bind(this));
window.addEventListener('resize', this.windowBlurHandler.bind(this));
// window.addEventListener('resize', this.windowBlurHandler.bind(this));
}
}, 10);
}

private readonly calculatePosition = (): void => {
const top = this.getCalculatedTop();
const left = this.getCalculatedLeft();
const width = this.getCalculatedWidth();

[
{ prop: 'width', value: width },
{ prop: 'top', value: top },
{ prop: 'left', value: left }
].forEach(({ prop, value }) => {
this.container.style.setProperty(prop, `${Math.round(value)}px`);
});
};

close = (): void => {
this.render.removeClass('mynah-overlay-open');
// In this timeout, we're waiting the close animation to be ended
Expand All @@ -189,19 +213,15 @@ export class Overlay {
window.removeEventListener('resize', this.windowBlurHandler.bind(this));
};

private readonly getCalculatedLeft = (
horizontalDirection: OverlayHorizontalDirection,
referenceElement?: HTMLElement | ExtendedHTMLElement,
referencePoint?: { top?: number; left: number }
): number => {
private readonly getCalculatedLeft = (): number => {
const referenceRectangle =
referenceElement !== undefined
? referenceElement.getBoundingClientRect()
: referencePoint !== undefined
? { left: referencePoint.left, width: 0 }
this.referenceElement !== undefined
? this.referenceElement.getBoundingClientRect()
: this.referencePoint !== undefined
? { left: this.referencePoint.left, width: 0 }
: { left: 0, width: 0 };

switch (horizontalDirection.toString()) {
switch (this.horizontalDirection.toString()) {
case OverlayHorizontalDirection.TO_RIGHT:
return referenceRectangle.left + referenceRectangle.width + OVERLAY_MARGIN;
case OverlayHorizontalDirection.START_TO_RIGHT:
Expand All @@ -217,27 +237,21 @@ export class Overlay {
}
};

private readonly getCalculatedWidth = (
referenceElement?: HTMLElement | ExtendedHTMLElement
): number => {
return referenceElement !== undefined
? referenceElement.getBoundingClientRect().width
private readonly getCalculatedWidth = (): number => {
return this.referenceElement !== undefined
? this.referenceElement.getBoundingClientRect().width
: 0;
};

private readonly getCalculatedTop = (
verticalDirection: OverlayVerticalDirection,
referenceElement?: HTMLElement | ExtendedHTMLElement,
referencePoint?: { top: number; left?: number }
): number => {
private readonly getCalculatedTop = (): number => {
const referenceRectangle =
referenceElement !== undefined
? referenceElement.getBoundingClientRect()
: referencePoint !== undefined
? { top: referencePoint.top, height: 0 }
this.referenceElement !== undefined
? this.referenceElement.getBoundingClientRect()
: this.referencePoint !== undefined
? { top: this.referencePoint.top, height: 0 }
: { top: 0, height: 0 };

switch (verticalDirection.toString()) {
switch (this.verticalDirection.toString()) {
case OverlayVerticalDirection.TO_BOTTOM:
return referenceRectangle.top + referenceRectangle.height + OVERLAY_MARGIN;
case OverlayVerticalDirection.START_TO_BOTTOM:
Expand Down
81 changes: 44 additions & 37 deletions src/styles/components/chat/_chat-command-selector.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
box-sizing: border-box;
background-color: var(--mynah-card-bg);
border-radius: var(--mynah-card-radius);
width: 100%;
pointer-events: all;
flex-flow: column nowrap;
align-items: stretch;
justify-content: flex-start;
max-height: 80vh;
overflow-x: hidden;
padding: var(--mynah-sizing-4);
overflow-y: auto;
margin-top: var(--mynah-sizing-4);
padding: 0 var(--mynah-sizing-4) var(--mynah-sizing-4) var(--mynah-sizing-4);
overflow: hidden auto;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
@include list-fader-bottom();
> .mynah-chat-command-selector-group {
display: flex;
Expand All @@ -22,15 +23,36 @@
align-items: stretch;
justify-content: flex-start;
gap: var(--mynah-sizing-1);
font-size: var(--mynah-font-size-medium);
> .mynah-chat-command-selector-group-title {
margin: 0;
color: var(--mynah-color-text-strong);
padding: 0 var(--mynah-sizing-3);
margin-bottom: var(--mynah-sizing-1);
position: relative;
border-radius: var(--mynah-input-radius);
padding: var(--mynah-sizing-3) var(--mynah-sizing-3) 0 var(--mynah-sizing-3);
margin-bottom: var(--mynah-sizing-3);
font-size: var(--mynah-font-size-large);
overflow: hidden;
position: sticky;
top: -1px;
background-color: var(--mynah-card-bg);
z-index: 1;
outline: solid var(--mynah-sizing-4) var(--mynah-card-bg);

&:before {
content: '';
position: absolute;
top: calc(var(--mynah-sizing-3) * -2);
left: calc(var(--mynah-sizing-3) * -2);
background-color: var(--mynah-card-bg);
z-index: -1;
right: calc(var(--mynah-sizing-3) * -2);
bottom: calc(var(--mynah-sizing-3) * -2);
padding: var(--mynah-sizing-3);
}

&.stuck {
box-shadow:
0px calc(var(--mynah-sizing-4) + 3px) 0 var(--mynah-card-bg),
0px calc(var(--mynah-sizing-4) + 4px) 0 rgba(0, 0, 0, 0.075);
}
}

& + .mynah-chat-command-selector-group {
Expand All @@ -44,7 +66,7 @@
position: relative;
box-sizing: border-box;
width: 100%;
flex-flow: row nowrap;
flex-flow: column nowrap;
align-items: flex-start;
justify-content: flex-start;
overflow: hidden;
Expand All @@ -53,8 +75,9 @@
color: var(--mynah-color-text-default);
border-radius: var(--mynah-input-radius);
transition: var(--mynah-short-transition-rev);
gap: var(--mynah-sizing-3);

gap: var(--mynah-sizing-1);
scroll-snap-align: center;
scroll-snap-stop: always;
&[disabled='true'] {
&::before {
border-color: transparent !important;
Expand All @@ -77,32 +100,16 @@
}
}
}
> .mynah-ui-icon {
margin-top: var(--mynah-sizing-1);
> .mynah-chat-command-selector-command-name {
font-family: var(--mynah-font-family);
font-size: var(--mynah-font-size-medium);
font-weight: bold;
flex: 0 1 0%;
}
> .mynah-chat-command-selector-command-description {
font-size: var(--mynah-font-size-small);
color: var(--mynah-color-text-weak);
}

> .mynah-chat-command-selector-command-container {
flex: 1;
display: flex;
position: relative;
box-sizing: border-box;
flex-flow: column nowrap;
align-items: flex-start;
justify-content: flex-start;
overflow: hidden;
gap: var(--mynah-sizing-1);

> .mynah-chat-command-selector-command-name {
font-family: var(--mynah-font-family);
font-weight: bold;
flex: 0 1 0%;
}
> .mynah-chat-command-selector-command-description {
color: var(--mynah-color-text-weak);
flex: 1 0 100%;
}
flex: 1 0 100%;
}
}
}
Expand All @@ -125,4 +132,4 @@
}
}
}
}
}
2 changes: 1 addition & 1 deletion ui-tests/src/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ body {
* {
font-stretch: 100%;
letter-spacing: normal;
text-rendering: optimizeSpeed;
text-rendering: optimizeLegibility !important;
-webkit-font-smoothing: antialiased;
}

Expand Down
Loading