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
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent, Ref } from 'react';
import {
CSSProperties,
useState,
Fragment,
FunctionComponent,
MouseEvent as ReactMouseEvent,
KeyboardEvent as ReactKeyboardEvent,
Ref
} from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';
import squareImg from './PF-social-color-square.svg';
Expand Down Expand Up @@ -44,6 +52,8 @@ export const BotMessageExample: FunctionComponent = () => {
return table;
case 'Image':
return image;
case 'Footnote':
return footnote;
default:
return;
}
Expand Down Expand Up @@ -150,6 +160,18 @@ _Italic text, formatted with single underscores_

const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;

const footnote = `This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote]

[^1]: This is a short footnote. To return the highlight to the original message, click the arrow.

[^bignote]: This is a long footnote with multiple paragraphs and formatting.

To break long footnotes into paragraphs, indent the text.

Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`.

> You can even include blockquotes in footnotes!`;

const error = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
Expand All @@ -165,8 +187,8 @@ _Italic text, formatted with single underscores_
)
};

const onSelect = (_event: MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
setVariant(value);
const onSelect = (_event: ReactMouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
setVariant(value as string);
setSelected(value as string);
setIsOpen(false);
if (value === 'Expandable code') {
Expand Down Expand Up @@ -196,6 +218,76 @@ _Italic text, formatted with single underscores_
</MenuToggle>
);

const handleFootnoteNavigation = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
const target = event.target as HTMLElement;

// Depending on whether it is a click event or keyboard event, target may be a link or something like a span
// Look for the closest anchor element (could be a parent)
const anchorElement = target.closest('a');
const href = anchorElement?.getAttribute('href');

// Check if this is a footnote link - we only have internal links in this example, so this is all we need here
if (href && href.startsWith('#')) {
// Prevent default behavior to avoid page re-render on click in PatternFly docs framework
event.preventDefault();

let targetElement: HTMLElement | null = null;
const targetId = href.replace('#', '');
targetElement = document.querySelector(`[id="${targetId}"]`);

if (targetElement) {
let focusTarget = targetElement;

// If we found a footnote definition container, focus on the parent li element
if (targetElement.id?.startsWith('bot-message-fn-')) {
// Find the parent li element that contains the footnote
const parentLi = targetElement.closest('li');
if (parentLi) {
focusTarget = parentLi as HTMLElement;
}
}

focusTarget.focus();

let elementToHighlight = targetElement;

// If this is a backref link (going back to footnote reference),
// we want to highlight more of the ref line and not just the link itself
// since the target is so small
if (targetElement.id?.startsWith('bot-message-fnref-')) {
const refLink = targetElement;

// Walk up the DOM to find a meaningful container
let parent = refLink.parentElement;
while (parent && parent.tagName.toLowerCase() !== 'p' && parent !== document.body) {
parent = parent.parentElement;
}

// Use if found, otherwise use the immediate parent or target as a fallback
elementToHighlight = parent || refLink.parentElement || targetElement;
}

// Briefly highlight the target element for fun to show what you can do
const originalBackground = elementToHighlight.style.backgroundColor;
const originalTransition = elementToHighlight.style.transition;

elementToHighlight.style.transition = 'background-color 0.3s ease';
elementToHighlight.style.backgroundColor = 'var(--pf-t--global--background--color--tertiary--default)';

setTimeout(() => {
elementToHighlight.style.backgroundColor = originalBackground;
setTimeout(() => {
elementToHighlight.style.transition = originalTransition;
}, 300);
}, 1000);
}
}
};

const onClick = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
handleFootnoteNavigation(event);
};

return (
<>
<Message
Expand Down Expand Up @@ -248,6 +340,7 @@ _Italic text, formatted with single underscores_
<SelectOption value="More complex list">More complex list</SelectOption>
<SelectOption value="Table">Table</SelectOption>
<SelectOption value="Image">Image</SelectOption>
<SelectOption value="Footnote">Footnote</SelectOption>
<SelectOption value="Error">Error</SelectOption>
</SelectList>
</Select>
Expand All @@ -265,6 +358,11 @@ _Italic text, formatted with single underscores_
// The purpose of this plugin is to provide unique link names for the code blocks
// Because they are in the same message, this requires a custom plugin to parse the syntax tree
additionalRehypePlugins={[rehypeCodeBlockToggle]}
linkProps={{ onClick }}
// clobberPrefix controls the label ids
reactMarkdownProps={{
remarkRehypeOptions: { footnoteLabel: 'Bot message footnotes', clobberPrefix: 'bot-message-' }
}}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Fragment, useState, useRef, useEffect, CSSProperties, FunctionComponent, MouseEvent, Ref } from 'react';
import {
Fragment,
useState,
useRef,
useEffect,
CSSProperties,
FunctionComponent,
MouseEvent as ReactMouseEvent,
KeyboardEvent as ReactKeyboardEvent,
Ref
} from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import userAvatar from './user_avatar.svg';
import {
Expand Down Expand Up @@ -64,6 +74,8 @@ export const UserMessageExample: FunctionComponent = () => {
return table;
case 'Image':
return image;
case 'Footnote':
return footnote;
default:
return '';
}
Expand Down Expand Up @@ -170,6 +182,18 @@ _Italic text, formatted with single underscores_

const image = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`;

const footnote = `This is some text that has a short footnote[^1] and this is text with a longer footnote.[^bignote]

[^1]: This is a short footnote. To return the highlight to the original message, click the arrow.

[^bignote]: This is a long footnote with multiple paragraphs and formatting.

To break long footnotes into paragraphs, indent the text.

Add as many paragraphs as you like. You can include *italic text*, **bold text**, and \`code\`.

> You can even include blockquotes in footnotes!`;

const error = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
Expand All @@ -185,7 +209,7 @@ _Italic text, formatted with single underscores_
)
};

const onSelect = (_event: MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
const onSelect = (_event: ReactMouseEvent<Element> | undefined, value: string | number | undefined) => {
setVariant(value);
setSelected(value as string);
setIsOpen(false);
Expand Down Expand Up @@ -221,6 +245,78 @@ _Italic text, formatted with single underscores_
</MenuToggle>
);

const handleFootnoteNavigation = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
const target = event.target as HTMLElement;

// Depending on whether it is a click event or keyboard event, target may be a link or something like a span
// Look for the closest anchor element (could be a parent)
const anchorElement = target.closest('a');
const href = anchorElement?.getAttribute('href');

// Check if this is a footnote link - we only have internal links in this example, so this is all we need here
if (href && href.startsWith('#')) {
// Prevent default behavior to avoid page re-render on click in PatternFly docs framework
event.preventDefault();

let targetElement: HTMLElement | null = null;
const targetId = href.replace('#', '');
targetElement = document.querySelector(`[id="${targetId}"]`);

if (targetElement) {
let focusTarget = targetElement;

// If we found a footnote definition container, focus on the parent li element
if (targetElement.id?.startsWith('user-message-fn-')) {
// Find the parent li element that contains the footnote
const parentLi = targetElement.closest('li');
if (parentLi) {
focusTarget = parentLi as HTMLElement;
}
}

focusTarget.focus();

let elementToHighlight = targetElement;
const searchStartElement = targetElement;
let elementToHighlightContainer: HTMLElement | null = null;

// For footnote references, look for an appropriate container
if (!targetElement.id?.startsWith('user-message-fn-')) {
let parent = searchStartElement.parentElement;
while (
parent &&
!(parent.tagName.toLowerCase() === 'span' && parent.classList.contains('pf-chatbot__message-text')) &&
parent !== document.body
) {
parent = parent.parentElement;
}
elementToHighlightContainer = parent;
}

// Use the found container if available, otherwise fall back to the target element
elementToHighlight = elementToHighlightContainer || targetElement;

// Briefly highlight the target element for fun to show what you can do
const originalBackground = elementToHighlight.style.backgroundColor;
const originalTransition = elementToHighlight.style.transition;

elementToHighlight.style.transition = 'background-color 0.3s ease';
elementToHighlight.style.backgroundColor = 'var(--pf-t--global--icon--color--brand--hover)';

setTimeout(() => {
elementToHighlight.style.backgroundColor = originalBackground;
setTimeout(() => {
elementToHighlight.style.transition = originalTransition;
}, 300);
}, 1000);
}
}
};

const onClick = (event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLElement>) => {
handleFootnoteNavigation(event);
};

return (
<>
<Message
Expand Down Expand Up @@ -270,6 +366,7 @@ _Italic text, formatted with single underscores_
<SelectOption value="More complex list">More complex list</SelectOption>
<SelectOption value="Table">Table</SelectOption>
<SelectOption value="Image">Image</SelectOption>
<SelectOption value="Footnote">Footnote</SelectOption>
<SelectOption value="Error">Error</SelectOption>
</SelectList>
</Select>
Expand All @@ -287,6 +384,14 @@ _Italic text, formatted with single underscores_
// The purpose of this plugin is to provide unique link names for the code blocks
// Because they are in the same message, this requires a custom plugin to parse the syntax tree
additionalRehypePlugins={[rehypeCodeBlockToggle]}
linkProps={{ onClick }}
// clobberPrefix controls the label ids
reactMarkdownProps={{
remarkRehypeOptions: {
footnoteLabel: 'User message footnotes',
clobberPrefix: 'user-message-'
}
}}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@
}

.pf-chatbot__message-inline-code {
--pf-chatbot-message-text-inline-code-font-size: var(--pf-t--global--font--size--body--default);
background-color: var(--pf-t--global--background--color--tertiary--default);
font-size: var(--pf-t--global--font--size--body--default);
font-size: var(--pf-chatbot-message-text-inline-code-font-size);
}

.pf-chatbot__message-code-toggle {
Expand Down
8 changes: 6 additions & 2 deletions packages/module/src/Message/LinkMessage/LinkMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import { Button, ButtonProps } from '@patternfly/react-core';
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
import { ExtraProps } from 'react-markdown';

const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & ExtraProps) => {
if (target === '_blank') {
return (
<Button
Expand All @@ -16,6 +17,8 @@ const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
iconPosition="end"
isInline
target={target}
// need to explicitly call this out or id doesn't seem to get passed - required for footnotes
id={id}
{...props}
>
{children}
Expand All @@ -24,7 +27,8 @@ const LinkMessage = ({ children, target, href, ...props }: ButtonProps) => {
}

return (
<Button isInline component="a" href={href} variant="link" {...props}>
// need to explicitly call this out or id doesn't seem to get passed - required for footnotes
<Button isInline component="a" href={href} variant="link" id={id} {...props}>
{children}
</Button>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/module/src/Message/ListMessage/ListItemMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import { ExtraProps } from 'react-markdown';
import { ListItem } from '@patternfly/react-core';

const ListItemMessage = ({ children }: JSX.IntrinsicElements['li'] & ExtraProps) => <ListItem>{children}</ListItem>;
const ListItemMessage = ({ children, ...props }: JSX.IntrinsicElements['li'] & ExtraProps) => (
<ListItem {...props} tabIndex={props?.id?.includes('fn-') ? -1 : props?.tabIndex}>
{children}
</ListItem>
);

export default ListItemMessage;
17 changes: 17 additions & 0 deletions packages/module/src/Message/ListMessage/ListMessage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,22 @@
background-color: var(--pf-t--global--color--brand--default);
color: var(--pf-t--global--text--color--on-brand--default);
padding: var(--pf-t--global--spacer--sm);

// prevents issues when highlighting things like footnotes - don't have blue on blue
.pf-chatbot__message-text {
background-color: initial;
}
}

// targets footnotes specifically and prevents misalignment problems
.footnotes {
li > span {
display: inline-flex;
flex-direction: column;
}
}

li a {
color: var(--pf-t--global--text--color--on-brand--default);
}
}
Loading
Loading