Skip to content

feat: make UI kit tooltips accessible #5911

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

Open
wants to merge 1 commit into
base: master
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
60 changes: 49 additions & 11 deletions examples/playground/src/playground/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
// (C) 2024 GoodData Corporation
// (C) 2024-2025 GoodData Corporation

import * as React from "react";
import { Dashboard } from "@gooddata/sdk-ui-dashboard";
// import { Dashboard } from "@gooddata/sdk-ui-dashboard";
import { BubbleHoverTrigger, Button, Bubble } from "@gooddata/sdk-ui-kit";

const dashboard = import.meta.env.VITE_DASHBOARD;
// const dashboard = import.meta.env.VITE_DASHBOARD;

export const Playground: React.FC = () => {
if (!dashboard) return null;

return (
<Dashboard
dashboard={dashboard}
config={{
initialRenderMode: "view",
}}
/>
<div style={{ padding: "10rem" }}>
<Button className="gd-button-secondary" value="First" />
<BubbleHoverTrigger className="gd-list-item-tooltip" showDelay={0} hideDelay={1500}>
<Button className="gd-button-secondary" value="Kit Button (non interactive tooltip)" />
<Bubble alignPoints={[{ align: "bc tl", offset: { x: 20, y: 25 } }]}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis
</Bubble>
</BubbleHoverTrigger>
<BubbleHoverTrigger className="gd-list-item-tooltip" showDelay={0} hideDelay={1500}>
<button className="gd-button-secondary">HTML Button (non interactive tooltip)</button>
<Bubble alignPoints={[{ align: "bc tl", offset: { x: 20, y: 25 } }]}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis
</Bubble>
</BubbleHoverTrigger>
<BubbleHoverTrigger
className="gd-list-item-tooltip"
showDelay={0}
hideDelay={1500}
isInteractive={true}
>
<button className="gd-button-secondary">HTML Button (interactive tooltip)</button>
<Bubble alignPoints={[{ align: "bc tl", offset: { x: 20, y: 25 } }]}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum ante ipsum primis in
<br />
<br />
<Button className="gd-button-secondary" value="Inside a tooltip" tabIndex={2} />
<br />
<br />
<a href="https://www.google.com" target="_blank" rel="noopener noreferrer" tabIndex={1}>
https://google.com
</a>
</Bubble>
</BubbleHoverTrigger>
<Button className="gd-button-secondary" value="Last" />
</div>
);
// if (!dashboard) return null;
//
// return (
// <Dashboard
// dashboard={dashboard}
// config={{
// initialRenderMode: "view",
// }}
// />
// );
};
26 changes: 26 additions & 0 deletions libs/sdk-ui-kit/api/sdk-ui-kit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,41 @@ export class BubbleHoverTrigger extends BubbleTrigger<IBubbleHoverTriggerProps>

// @internal (undocumented)
export class BubbleTrigger<P extends IBubbleTriggerProps> extends React_2.PureComponent<P, IBubbleTriggerState> {
// (undocumented)
bubbleRef: React_2.RefObject<HTMLDivElement>;
// (undocumented)
cancelClose: () => void;
// (undocumented)
protected changeBubbleVisibility(active: boolean): void;
// (undocumented)
closeBubble: () => void;
// (undocumented)
closeTimeout: number | null;
// (undocumented)
componentDidUpdate(_prevProps: IBubbleTriggerProps, prevState: IBubbleTriggerState): void;
// (undocumented)
static defaultProps: IBubbleTriggerProps;
// (undocumented)
protected eventListeners(): any;
// (undocumented)
handleKeyDown: (e: React_2.KeyboardEvent) => void;
// (undocumented)
render(): React_2.JSX.Element;
// (undocumented)
readonly state: Readonly<IBubbleTriggerState>;
// (undocumented)
triggerRef: React_2.RefObject<HTMLButtonElement>;
}

// @internal (undocumented)
export class Button extends React_2.Component<IButtonProps> {
// (undocumented)
buttonNode: HTMLElement;
// (undocumented)
componentDidMount(): void;
// (undocumented)
componentDidUpdate(prevProps: Readonly<IButtonProps>): void;
// (undocumented)
static defaultProps: {
className: string;
disabled: boolean;
Expand Down Expand Up @@ -335,6 +353,8 @@ export class ConfirmDialog extends PureComponent<IConfirmDialogBaseProps> {

// @internal (undocumented)
export class ConfirmDialogBase extends DialogBase<IConfirmDialogBaseProps> {
// (undocumented)
componentDidMount(): void;
// (undocumented)
static defaultProps: IConfirmDialogBaseProps;
// (undocumented)
Expand Down Expand Up @@ -1016,6 +1036,8 @@ export interface IBubbleTriggerProps {
// (undocumented)
eventsOnBubble?: boolean;
// (undocumented)
isInteractive?: boolean;
// (undocumented)
onBubbleClose?: () => void;
// (undocumented)
onBubbleOpen?: () => void;
Expand Down Expand Up @@ -1054,6 +1076,8 @@ export interface IButtonProps {
// (undocumented)
ariaLabel?: string;
// (undocumented)
autoFocus?: boolean;
// (undocumented)
children?: ReactNode;
// (undocumented)
className?: string;
Expand Down Expand Up @@ -1194,6 +1218,8 @@ export const Icon: Record<string, React_2.FC<IIconProps>>;

// @internal (undocumented)
export interface IConfirmDialogBaseProps extends IDialogBaseProps {
// (undocumented)
autoFocus?: boolean;
// (undocumented)
cancelButtonText?: string;
// (undocumented)
Expand Down
21 changes: 19 additions & 2 deletions libs/sdk-ui-kit/src/Bubble/Bubble.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// (C) 2020-2024 GoodData Corporation
import React from "react";
// (C) 2020-2025 GoodData Corporation
import React, { createRef } from "react";
import keys from "lodash/keys.js";
import cloneDeep from "lodash/cloneDeep.js";
import isEqual from "lodash/isEqual.js";
Expand Down Expand Up @@ -78,6 +78,11 @@ export interface IBubbleProps {
overlayClassName?: string;
children?: React.ReactNode;
overlayPositionType?: OverlayPositionType;

// TODO accessibility config encapsulation?
isInteractive?: boolean;
id?: string;
onBlur?: React.FocusEventHandler<HTMLDivElement>;
}

/**
Expand Down Expand Up @@ -116,6 +121,8 @@ export class Bubble extends React.Component<IBubbleProps, IBubbleState> {
arrowOffsets: ArrowOffsets;
arrowDirections: ArrowDirections;

private overlayRef = createRef<Overlay>();

constructor(props: IBubbleProps) {
super(props);

Expand Down Expand Up @@ -181,11 +188,17 @@ export class Bubble extends React.Component<IBubbleProps, IBubbleState> {
}, this);
}

// replace with useImperativeHandle hook if the component is refactored to fn component
public getRefs = () => {
return this.overlayRef.current.getRefs();
};

render() {
const arrowStyle = result(this.props, "arrowStyle", {});

return (
<Overlay
ref={this.overlayRef}
className={this.props.overlayClassName}
alignTo={this.props.alignTo}
onAlign={this.onAlign}
Expand All @@ -197,6 +210,10 @@ export class Bubble extends React.Component<IBubbleProps, IBubbleState> {
ignoreClicksOnByClass={this.props.ignoreClicksOnByClass}
onClose={this.props.onClose}
positionType={this.props.overlayPositionType}
tabIndex={this.props.isInteractive ? -1 : undefined}
role="tooltip"
id={this.props.id}
onBlur={this.props.onBlur}
>
<div
onMouseEnter={this.props.onMouseEnter}
Expand Down
Loading
Loading