Skip to content

Commit 0500609

Browse files
TrevorBurnhamKuai Hu
authored andcommitted
feat: Add support for download links in ButtonDropdown (cloudscape-design#3694)
1 parent fd77494 commit 0500609

File tree

4 files changed

+229
-1
lines changed

4 files changed

+229
-1
lines changed

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5615,6 +5615,7 @@ The following properties are supported across all types:
56155615
### action
56165616

56175617
- \`href\` (string) - (Optional) Defines the target URL of the menu item, turning it into a link.
5618+
- \`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.
56185619
- \`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.
56195620
- \`externalIconAriaLabel\` (string) - Adds an \`aria-label\` to the external icon.
56205621
- \`iconName\` (string) - (Optional) Specifies the name of the icon, used with the [icon component](/components/icon/).

src/button-dropdown/__tests__/button-dropdown-items.test.tsx

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,227 @@ describe('Internal ButtonDropdown badge property', () => {
519519
expect(wrapper.findLabelTag()).toBeNull();
520520
});
521521
});
522+
523+
describe('ButtonDropdown download property', () => {
524+
const testProps = { expandToViewport: false };
525+
526+
it('should render download attribute with boolean true value', () => {
527+
const items: ButtonDropdownProps.Items = [
528+
{
529+
id: 'download-item',
530+
text: 'Download file',
531+
href: 'https://example.com/file.pdf',
532+
download: true,
533+
},
534+
];
535+
const wrapper = renderButtonDropdown({ ...testProps, items });
536+
wrapper.openDropdown();
537+
538+
const anchor = wrapper.findItemById('download-item')!.find('a')!.getElement();
539+
expect(anchor).toHaveAttribute('download', '');
540+
expect(anchor).toHaveAttribute('href', 'https://example.com/file.pdf');
541+
});
542+
543+
it('should render download attribute with string value', () => {
544+
const items: ButtonDropdownProps.Items = [
545+
{
546+
id: 'download-item',
547+
text: 'Download file',
548+
href: 'https://example.com/file.pdf',
549+
download: 'custom-filename.pdf',
550+
},
551+
];
552+
const wrapper = renderButtonDropdown({ ...testProps, items });
553+
wrapper.openDropdown();
554+
555+
const anchor = wrapper.findItemById('download-item')!.find('a')!.getElement();
556+
expect(anchor).toHaveAttribute('download', 'custom-filename.pdf');
557+
expect(anchor).toHaveAttribute('href', 'https://example.com/file.pdf');
558+
});
559+
560+
it('should not render download attribute when download is false', () => {
561+
const items: ButtonDropdownProps.Items = [
562+
{
563+
id: 'no-download-item',
564+
text: 'Regular link',
565+
href: 'https://example.com/page',
566+
download: false,
567+
},
568+
];
569+
const wrapper = renderButtonDropdown({ ...testProps, items });
570+
wrapper.openDropdown();
571+
572+
const anchor = wrapper.findItemById('no-download-item')!.find('a')!.getElement();
573+
expect(anchor).not.toHaveAttribute('download');
574+
expect(anchor).toHaveAttribute('href', 'https://example.com/page');
575+
});
576+
577+
it('should not render download attribute when download is not specified', () => {
578+
const items: ButtonDropdownProps.Items = [
579+
{
580+
id: 'regular-item',
581+
text: 'Regular link',
582+
href: 'https://example.com/page',
583+
},
584+
];
585+
const wrapper = renderButtonDropdown({ ...testProps, items });
586+
wrapper.openDropdown();
587+
588+
const anchor = wrapper.findItemById('regular-item')!.find('a')!.getElement();
589+
expect(anchor).not.toHaveAttribute('download');
590+
expect(anchor).toHaveAttribute('href', 'https://example.com/page');
591+
});
592+
593+
it('should not render download attribute when href is not provided', () => {
594+
const items: ButtonDropdownProps.Items = [
595+
{
596+
id: 'no-href-item',
597+
text: 'Button item',
598+
download: true,
599+
},
600+
];
601+
const wrapper = renderButtonDropdown({ ...testProps, items });
602+
wrapper.openDropdown();
603+
604+
const item = wrapper.findItemById('no-href-item')!;
605+
expect(item.find('a')).toBe(null);
606+
expect(item.getElement()).toHaveTextContent('Button item');
607+
});
608+
609+
it('should not render download attribute when item is disabled', () => {
610+
const items: ButtonDropdownProps.Items = [
611+
{
612+
id: 'disabled-download-item',
613+
text: 'Disabled download',
614+
href: 'https://example.com/file.pdf',
615+
download: 'filename.pdf',
616+
disabled: true,
617+
},
618+
];
619+
const wrapper = renderButtonDropdown({ ...testProps, items });
620+
wrapper.openDropdown();
621+
622+
const anchor = wrapper.findItemById('disabled-download-item')!.find('a')!.getElement();
623+
expect(anchor).not.toHaveAttribute('download');
624+
expect(anchor).not.toHaveAttribute('href');
625+
});
626+
627+
it('should not render download attribute when parent category is disabled', () => {
628+
const items: ButtonDropdownProps.Items = [
629+
{
630+
text: 'Disabled category',
631+
disabled: true,
632+
items: [
633+
{
634+
id: 'category-download-item',
635+
text: 'Download in disabled category',
636+
href: 'https://example.com/file.pdf',
637+
download: true,
638+
},
639+
],
640+
},
641+
];
642+
const wrapper = renderButtonDropdown({ ...testProps, items });
643+
wrapper.openDropdown();
644+
645+
const anchor = wrapper.findItemById('category-download-item')!.find('a')!.getElement();
646+
expect(anchor).not.toHaveAttribute('download');
647+
expect(anchor).not.toHaveAttribute('href');
648+
});
649+
650+
it('should render download attribute with external link', () => {
651+
const items: ButtonDropdownProps.Items = [
652+
{
653+
id: 'external-download-item',
654+
text: 'Download external file',
655+
href: 'https://external.com/file.pdf',
656+
download: 'external-file.pdf',
657+
external: true,
658+
externalIconAriaLabel: '(opens new tab)',
659+
},
660+
];
661+
const wrapper = renderButtonDropdown({ ...testProps, items });
662+
wrapper.openDropdown();
663+
664+
const anchor = wrapper.findItemById('external-download-item')!.find('a')!.getElement();
665+
expect(anchor).toHaveAttribute('download', 'external-file.pdf');
666+
expect(anchor).toHaveAttribute('href', 'https://external.com/file.pdf');
667+
expect(anchor).toHaveAttribute('target', '_blank');
668+
expect(anchor).toHaveAttribute('rel', 'noopener noreferrer');
669+
});
670+
671+
it('should render download attribute in nested category items', () => {
672+
const items: ButtonDropdownProps.Items = [
673+
{
674+
text: 'Downloads',
675+
items: [
676+
{
677+
id: 'nested-download-item',
678+
text: 'Download nested file',
679+
href: 'https://example.com/nested-file.pdf',
680+
download: 'nested-filename.pdf',
681+
},
682+
{
683+
id: 'nested-regular-item',
684+
text: 'Regular nested link',
685+
href: 'https://example.com/page',
686+
},
687+
],
688+
},
689+
];
690+
const wrapper = renderButtonDropdown({ ...testProps, items });
691+
wrapper.openDropdown();
692+
693+
const downloadAnchor = wrapper.findItemById('nested-download-item')!.find('a')!.getElement();
694+
expect(downloadAnchor).toHaveAttribute('download', 'nested-filename.pdf');
695+
expect(downloadAnchor).toHaveAttribute('href', 'https://example.com/nested-file.pdf');
696+
697+
const regularAnchor = wrapper.findItemById('nested-regular-item')!.find('a')!.getElement();
698+
expect(regularAnchor).not.toHaveAttribute('download');
699+
expect(regularAnchor).toHaveAttribute('href', 'https://example.com/page');
700+
});
701+
702+
it('should handle mixed items with and without download', () => {
703+
const items: ButtonDropdownProps.Items = [
704+
{
705+
id: 'download-boolean',
706+
text: 'Download with boolean',
707+
href: 'https://example.com/file1.pdf',
708+
download: true,
709+
},
710+
{
711+
id: 'download-string',
712+
text: 'Download with string',
713+
href: 'https://example.com/file2.pdf',
714+
download: 'custom-name.pdf',
715+
},
716+
{
717+
id: 'regular-link',
718+
text: 'Regular link',
719+
href: 'https://example.com/page',
720+
},
721+
{
722+
id: 'button-item',
723+
text: 'Button item',
724+
},
725+
];
726+
const wrapper = renderButtonDropdown({ ...testProps, items });
727+
wrapper.openDropdown();
728+
729+
// Check download with boolean
730+
const booleanAnchor = wrapper.findItemById('download-boolean')!.find('a')!.getElement();
731+
expect(booleanAnchor).toHaveAttribute('download', '');
732+
733+
// Check download with string
734+
const stringAnchor = wrapper.findItemById('download-string')!.find('a')!.getElement();
735+
expect(stringAnchor).toHaveAttribute('download', 'custom-name.pdf');
736+
737+
// Check regular link
738+
const regularAnchor = wrapper.findItemById('regular-link')!.find('a')!.getElement();
739+
expect(regularAnchor).not.toHaveAttribute('download');
740+
741+
// Check button item (no anchor)
742+
const buttonItem = wrapper.findItemById('button-item')!;
743+
expect(buttonItem.find('a')).toBe(null);
744+
});
745+
});

src/button-dropdown/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface ButtonDropdownProps extends BaseComponentProps, ExpandToViewpor
3333
* ### action
3434
*
3535
* - `href` (string) - (Optional) Defines the target URL of the menu item, turning it into a link.
36+
* - `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.
3637
* - `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.
3738
* - `externalIconAriaLabel` (string) - Adds an `aria-label` to the external icon.
3839
* - `iconName` (string) - (Optional) Specifies the name of the icon, used with the [icon component](/components/icon/).
@@ -198,6 +199,7 @@ export namespace ButtonDropdownProps {
198199
*/
199200
description?: string;
200201
href?: string;
202+
download?: boolean | string;
201203
external?: boolean;
202204
externalIconAriaLabel?: string;
203205
iconAlt?: string;
@@ -208,7 +210,7 @@ export namespace ButtonDropdownProps {
208210
}
209211

210212
export interface CheckboxItem
211-
extends Omit<ButtonDropdownProps.Item, 'href' | 'external' | 'externalIconAriaLabel' | 'itemType'> {
213+
extends Omit<ButtonDropdownProps.Item, 'href' | 'download' | 'external' | 'externalIconAriaLabel' | 'itemType'> {
212214
itemType: 'checkbox';
213215
checked: boolean;
214216
}

src/button-dropdown/item-element/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ function MenuItem({ item, disabled, highlighted, linkStyle }: MenuItemProps) {
136136
<a
137137
{...menuItemProps}
138138
href={!disabled ? item.href : undefined}
139+
download={!disabled && item.download ? item.download : undefined}
139140
target={getItemTarget(item)}
140141
rel={item.external ? 'noopener noreferrer' : undefined}
141142
>

0 commit comments

Comments
 (0)