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

Add tooltip Component for disabled buttons #4740

Merged
merged 17 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
65 changes: 64 additions & 1 deletion jsapp/js/components/common/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,74 @@
import React from 'react';
import type {ComponentStory, ComponentMeta} from '@storybook/react';
import Button from './button';
import type {ButtonType, ButtonColor, ButtonSize} from './button';
import type {TooltipAlignment} from './tooltip';
import {IconNames} from 'jsapp/fonts/k-icons';

const buttonTypes: ButtonType[] = ['bare', 'frame', 'full'];

const buttonColors: ButtonColor[] = [
'blue',
'light-blue',
'red',
'storm',
'cloud',
'dark-red',
'dark-blue',
];

const buttonSizes: ButtonSize[] = ['s', 'm', 'l'];

const tooltipPositions: TooltipAlignment[] = ['right', 'left', 'center'];

export default {
title: 'common/Button',
component: Button,
argTypes: {},
argTypes: {
type: {
description: 'Type of button',
options: buttonTypes,
control: 'select',
},
color: {
description: 'Color of button',
options: buttonColors,
control: 'select',
},
size: {
description: 'Size of button',
options: buttonSizes,
control: 'radio',
},
startIcon: {
description: 'Icon on the beginning (please use only one of the icons)',
options: Object.keys(IconNames),
control: {type: 'select'},
},
endIcon: {
description: 'Icon on the end (please use only one of the icons)',
options: Object.keys(IconNames),
control: {type: 'select'},
},
label: {
control: 'text',
},
tooltip: {
description: 'Tooltip text',
control: 'text',
},
tooltipPosition: {
description: 'Position of the tooltip (optional)',
options: tooltipPositions,
control: 'radio',
},
isDisabled: {control: 'boolean'},
isPending: {control: 'boolean'},
isFullWidth: {
description: 'Makes the button take 100% width of the container',
control: 'boolean',
},
},
} as ComponentMeta<typeof Button>;

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
Expand Down
58 changes: 35 additions & 23 deletions jsapp/js/components/common/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {IconName} from 'jsapp/fonts/k-icons';
import type {IconSize} from 'js/components/common/icon';
import Icon from 'js/components/common/icon';
import './button.scss';
import Tooltip, { TooltipAlignment } from './tooltip';

/**
* Note: we use a simple TypeScript types here instead of enums, so we don't
Expand Down Expand Up @@ -61,6 +62,8 @@ export interface ButtonProps {
* for icon-only buttons.
*/
tooltip?: string;
/** Sets the alignment of the tooltip */
tooltipPosition?: TooltipAlignment;
isDisabled?: boolean;
/** Changes the appearance to display spinner. */
isPending?: boolean;
Expand Down Expand Up @@ -134,9 +137,6 @@ const Button = (props: ButtonProps) => {

// For the attributes that don't have a falsy value.
const additionalButtonAttributes: AdditionalButtonAttributes = {};
if (props.tooltip) {
additionalButtonAttributes['data-tip'] = props.tooltip;
}
if (props['data-cy']) {
additionalButtonAttributes['data-cy'] = props['data-cy'];
}
Expand All @@ -154,27 +154,39 @@ const Button = (props: ButtonProps) => {
};

return (
<button
className={classNames.join(' ')}
type={props.isSubmit ? 'submit' : 'button'}
aria-disabled={props.isDisabled}
onClick={handleClick}
onKeyUp={onKeyUp}
{...additionalButtonAttributes}
>
{props.startIcon && <Icon name={props.startIcon} size={iconSize} />}

{props.label && <span className='k-button__label'>{props.label}</span>}

{/* Ensures only one icon is being displayed.*/}
{!props.startIcon && props.endIcon && (
<Icon name={props.endIcon} size={iconSize} />
)}

{props.isPending && (
<Icon name='spinner' size={iconSize} classNames={['k-spin']} />
<>
{props.tooltip !== undefined && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that if you don't add tooltip, this renders nothing. We need the button to be rendered with or without a tooltip :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that somethign is wrong with Storybook, because it wasn't rendering anything for Button, so I've added a commit that improves button.stories.tsx. Please take a look at what it does when testing the tooltip:
Screenshot 2023-12-13 at 15 19 35

<Tooltip
text={props.tooltip}
ariaLabel={props.tooltip}
alignment={props.tooltipPosition}
>
<button
className={classNames.join(' ')}
type={props.isSubmit ? 'submit' : 'button'}
aria-disabled={props.isDisabled}
onClick={handleClick}
onKeyUp={onKeyUp}
{...additionalButtonAttributes}
>
{props.startIcon && <Icon name={props.startIcon} size={iconSize} />}

{props.label && (
<span className='k-button__label'>{props.label}</span>
)}

{/* Ensures only one icon is being displayed.*/}
{!props.startIcon && props.endIcon && (
<Icon name={props.endIcon} size={iconSize} />
)}

{props.isPending && (
<Icon name='spinner' size={iconSize} classNames={['k-spin']} />
)}
</button>
</Tooltip>
)}
</button>
</>
);
};

Expand Down
54 changes: 54 additions & 0 deletions jsapp/js/components/common/tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import type {Meta, Story} from '@storybook/react';

import type {TooltipProps} from './tooltip';
import Tooltip from './tooltip';

export default {
title: 'Common/Tooltip',
component: Tooltip,
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
description:
'This is a component that displays a tooltip on a button that is hovered over.',
argTypes: {
text: {
description: 'Content of the tooltip shown on hover over button',
},
alignment: {
description:
'Position of the tooltip (centered as default)',
options: ['right', 'left', 'center'],
control: 'select',
defaultValue: 'center',
},
ariaLabel: {
description: 'Accessible label for screen readers',
},
},
} as Meta;

const Template: Story<TooltipProps> = (args) => (
<Tooltip {...args}>
<button>Your Button</button>
</Tooltip>
);

export const Default = Template.bind({});
Default.args = {
text: 'Default Tooltip Text',
alignment: 'center',
ariaLabel: 'Default Tooltip Text',
};

export const Right = Template.bind({});
Right.args = {
text: 'Right Aligned Tooltip Text',
alignment: 'right',
ariaLabel: 'Right Aligned Tooltip Text',
};

export const Left = Template.bind({});
Left.args = {
text: 'Left Aligned Tooltip Text',
alignment: 'left',
ariaLabel: 'Left Aligned Tooltip Text',
};
34 changes: 34 additions & 0 deletions jsapp/js/components/common/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

export type TooltipAlignment = 'right' | 'left' | 'center';

export interface TooltipProps {
/** Content of the tooltip */
text: string;
/** Accessible label for screen readers */
ariaLabel: string;
/** Position of the tooltip (centered as default) */
alignment?: TooltipAlignment;
}

/**
* Tooltip component that is necessary for managing dynamic content and
* accessibility features, such as readability. While we have the tooltip.scss
* component, that provides styling but not other functionaltiy such as
* allowing tooltips to work on disabled buttons.
*/
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
const Tooltip: React.FC<TooltipProps> = ({
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
text,
ariaLabel,
alignment,
children,
}) => (
<span
data-tip={text}
magicznyleszek marked this conversation as resolved.
Show resolved Hide resolved
className={`${alignment || 'center'}-tooltip`}
aria-label={ariaLabel}
>
{children}
</span>
);
export default Tooltip;
56 changes: 28 additions & 28 deletions jsapp/js/projects/projectsTable/projectBulkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,38 +33,38 @@ export default function ProjectBulkActions(props: ProjectBulkActionsProps) {
return (
<div className={actionsStyles.root}>
{/* Archive / Unarchive - Bulk action not supported yet */}
<span data-tip={t('Archive/Unarchive')} className='right-tooltip'>
<Button
isDisabled
type='bare'
color='storm'
size='s'
startIcon='archived'
/>
</span>
<Button
isDisabled
type='bare'
color='storm'
size='s'
startIcon='archived'
tooltip={t('Archive/Unarchive')}
tooltipPosition='right'
/>

{/* Share - Bulk action not supported yet */}
<span data-tip={t('Share projects')} className='right-tooltip'>
<Button
isDisabled
type='bare'
color='storm'
size='s'
startIcon='user-share'
/>
</span>
<Button
isDisabled
type='bare'
color='storm'
size='s'
startIcon='user-share'
tooltip={t('Share projects')}
tooltipPosition='right'
/>

{/* Delete */}
<span data-tip={tooltipForDelete} className='right-tooltip'>
<Button
isDisabled={!canBulkDelete}
type='bare'
color='storm'
size='s'
startIcon='trash'
onClick={() => setIsDeletePromptOpen(true)}
/>
</span>
<Button
isDisabled={!canBulkDelete}
type='bare'
color='storm'
size='s'
startIcon='trash'
onClick={() => setIsDeletePromptOpen(true)}
tooltip={tooltipForDelete}
tooltipPosition='right'
/>

{isDeletePromptOpen && (
<BulkDeletePrompt
Expand Down
Loading
Loading