Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -4567,6 +4567,7 @@ The following properties are supported across all types:
### action

- \`href\` (string) - (Optional) Defines the target URL of the menu item, turning it into a link.
- \`download\` (boolean | string) - (Optional) Indicates that the link should be downloaded when clicked. Only works when \`href\` is also provided. If set to \`true\`, the browser will use the filename from the URL. If set to a string, that string will be used as the suggested filename.
- \`external\` (boolean) - Marks a menu item as external by adding an icon after the menu item text. The link will open in a new tab when clicked. Note that this only works when \`href\` is also provided.
- \`externalIconAriaLabel\` (string) - Adds an \`aria-label\` to the external icon.
- \`iconName\` (string) - (Optional) Specifies the name of the icon, used with the [icon component](/components/icon/).
Expand Down
224 changes: 224 additions & 0 deletions src/button-dropdown/__tests__/button-dropdown-items.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,227 @@ describe('Internal ButtonDropdown badge property', () => {
expect(wrapper.findAllByClassName(iconStyles.badge)?.map(item => item.getElement())).toHaveLength(2);
});
});

describe('ButtonDropdown download property', () => {
const testProps = { expandToViewport: false };

it('should render download attribute with boolean true value', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'download-item',
text: 'Download file',
href: 'https://example.com/file.pdf',
download: true,
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('download-item')!.find('a')!.getElement();
expect(anchor).toHaveAttribute('download', '');
expect(anchor).toHaveAttribute('href', 'https://example.com/file.pdf');
});

it('should render download attribute with string value', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'download-item',
text: 'Download file',
href: 'https://example.com/file.pdf',
download: 'custom-filename.pdf',
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('download-item')!.find('a')!.getElement();
expect(anchor).toHaveAttribute('download', 'custom-filename.pdf');
expect(anchor).toHaveAttribute('href', 'https://example.com/file.pdf');
});

it('should not render download attribute when download is false', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'no-download-item',
text: 'Regular link',
href: 'https://example.com/page',
download: false,
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('no-download-item')!.find('a')!.getElement();
expect(anchor).not.toHaveAttribute('download');
expect(anchor).toHaveAttribute('href', 'https://example.com/page');
});

it('should not render download attribute when download is not specified', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'regular-item',
text: 'Regular link',
href: 'https://example.com/page',
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('regular-item')!.find('a')!.getElement();
expect(anchor).not.toHaveAttribute('download');
expect(anchor).toHaveAttribute('href', 'https://example.com/page');
});

it('should not render download attribute when href is not provided', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'no-href-item',
text: 'Button item',
download: true,
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const item = wrapper.findItemById('no-href-item')!;
expect(item.find('a')).toBe(null);
expect(item.getElement()).toHaveTextContent('Button item');
});

it('should not render download attribute when item is disabled', () => {
Copy link
Member

Choose a reason for hiding this comment

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

The link is already not clickable when disabled. Why do we want to remove the download attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Honestly, Claude added this. I agree that it doesn't really matter. The Button component keeps the download attribute when it's disabled.

const items: ButtonDropdownProps.Items = [
{
id: 'disabled-download-item',
text: 'Disabled download',
href: 'https://example.com/file.pdf',
download: 'filename.pdf',
disabled: true,
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('disabled-download-item')!.find('a')!.getElement();
expect(anchor).not.toHaveAttribute('download');
expect(anchor).not.toHaveAttribute('href');
});

it('should not render download attribute when parent category is disabled', () => {
const items: ButtonDropdownProps.Items = [
{
text: 'Disabled category',
disabled: true,
items: [
{
id: 'category-download-item',
text: 'Download in disabled category',
href: 'https://example.com/file.pdf',
download: true,
},
],
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('category-download-item')!.find('a')!.getElement();
expect(anchor).not.toHaveAttribute('download');
expect(anchor).not.toHaveAttribute('href');
});

it('should render download attribute with external link', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'external-download-item',
text: 'Download external file',
href: 'https://external.com/file.pdf',
download: 'external-file.pdf',
external: true,
externalIconAriaLabel: '(opens new tab)',
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const anchor = wrapper.findItemById('external-download-item')!.find('a')!.getElement();
expect(anchor).toHaveAttribute('download', 'external-file.pdf');
expect(anchor).toHaveAttribute('href', 'https://external.com/file.pdf');
expect(anchor).toHaveAttribute('target', '_blank');
expect(anchor).toHaveAttribute('rel', 'noopener noreferrer');
});

it('should render download attribute in nested category items', () => {
const items: ButtonDropdownProps.Items = [
{
text: 'Downloads',
items: [
{
id: 'nested-download-item',
text: 'Download nested file',
href: 'https://example.com/nested-file.pdf',
download: 'nested-filename.pdf',
},
{
id: 'nested-regular-item',
text: 'Regular nested link',
href: 'https://example.com/page',
},
],
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

const downloadAnchor = wrapper.findItemById('nested-download-item')!.find('a')!.getElement();
expect(downloadAnchor).toHaveAttribute('download', 'nested-filename.pdf');
expect(downloadAnchor).toHaveAttribute('href', 'https://example.com/nested-file.pdf');

const regularAnchor = wrapper.findItemById('nested-regular-item')!.find('a')!.getElement();
expect(regularAnchor).not.toHaveAttribute('download');
expect(regularAnchor).toHaveAttribute('href', 'https://example.com/page');
});

it('should handle mixed items with and without download', () => {
const items: ButtonDropdownProps.Items = [
{
id: 'download-boolean',
text: 'Download with boolean',
href: 'https://example.com/file1.pdf',
download: true,
},
{
id: 'download-string',
text: 'Download with string',
href: 'https://example.com/file2.pdf',
download: 'custom-name.pdf',
},
{
id: 'regular-link',
text: 'Regular link',
href: 'https://example.com/page',
},
{
id: 'button-item',
text: 'Button item',
},
];
const wrapper = renderButtonDropdown({ ...testProps, items });
wrapper.openDropdown();

// Check download with boolean
const booleanAnchor = wrapper.findItemById('download-boolean')!.find('a')!.getElement();
expect(booleanAnchor).toHaveAttribute('download', '');

// Check download with string
const stringAnchor = wrapper.findItemById('download-string')!.find('a')!.getElement();
expect(stringAnchor).toHaveAttribute('download', 'custom-name.pdf');

// Check regular link
const regularAnchor = wrapper.findItemById('regular-link')!.find('a')!.getElement();
expect(regularAnchor).not.toHaveAttribute('download');

// Check button item (no anchor)
const buttonItem = wrapper.findItemById('button-item')!;
expect(buttonItem.find('a')).toBe(null);
});
});
4 changes: 3 additions & 1 deletion src/button-dropdown/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ButtonDropdownProps extends BaseComponentProps, ExpandToViewpor
* ### action
*
* - `href` (string) - (Optional) Defines the target URL of the menu item, turning it into a link.
* - `download` (boolean | string) - (Optional) Indicates that the link should be downloaded when clicked. Only works when `href` is also provided. If set to `true`, the browser will use the filename from the URL. If set to a string, that string will be used as the suggested filename.
* - `external` (boolean) - Marks a menu item as external by adding an icon after the menu item text. The link will open in a new tab when clicked. Note that this only works when `href` is also provided.
* - `externalIconAriaLabel` (string) - Adds an `aria-label` to the external icon.
* - `iconName` (string) - (Optional) Specifies the name of the icon, used with the [icon component](/components/icon/).
Expand Down Expand Up @@ -156,6 +157,7 @@ export namespace ButtonDropdownProps {
disabledReason?: string;
description?: string;
href?: string;
download?: boolean | string;
external?: boolean;
externalIconAriaLabel?: string;
iconAlt?: string;
Expand All @@ -165,7 +167,7 @@ export namespace ButtonDropdownProps {
}

export interface CheckboxItem
extends Omit<ButtonDropdownProps.Item, 'href' | 'external' | 'externalIconAriaLabel' | 'itemType'> {
extends Omit<ButtonDropdownProps.Item, 'href' | 'download' | 'external' | 'externalIconAriaLabel' | 'itemType'> {
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if we need a visual clue for download similar to external.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In practice, I think we'd want to use iconName="download" whenever we use the download attribute. But I don't think that should be enforced; someone might want to use a different (custom?) icon to differentiate some downloads.

Having download and iconName as orthogonal options matches the API of the Button component.

itemType: 'checkbox';
checked: boolean;
}
Expand Down
1 change: 1 addition & 0 deletions src/button-dropdown/item-element/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ function MenuItem({ item, disabled, highlighted, linkStyle }: MenuItemProps) {
<a
{...menuItemProps}
href={!disabled ? item.href : undefined}
download={!disabled && item.download ? item.download : undefined}
target={getItemTarget(item)}
rel={item.external ? 'noopener noreferrer' : undefined}
>
Expand Down
Loading