diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index e37d26dc71..6c6a435ec9 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -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/). diff --git a/src/button-dropdown/__tests__/button-dropdown-items.test.tsx b/src/button-dropdown/__tests__/button-dropdown-items.test.tsx index 31cc3a178d..23d9819535 100644 --- a/src/button-dropdown/__tests__/button-dropdown-items.test.tsx +++ b/src/button-dropdown/__tests__/button-dropdown-items.test.tsx @@ -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', () => { + 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); + }); +}); diff --git a/src/button-dropdown/interfaces.ts b/src/button-dropdown/interfaces.ts index a96a30d123..5df1e911bb 100644 --- a/src/button-dropdown/interfaces.ts +++ b/src/button-dropdown/interfaces.ts @@ -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/). @@ -156,6 +157,7 @@ export namespace ButtonDropdownProps { disabledReason?: string; description?: string; href?: string; + download?: boolean | string; external?: boolean; externalIconAriaLabel?: string; iconAlt?: string; @@ -165,7 +167,7 @@ export namespace ButtonDropdownProps { } export interface CheckboxItem - extends Omit { + extends Omit { itemType: 'checkbox'; checked: boolean; } diff --git a/src/button-dropdown/item-element/index.tsx b/src/button-dropdown/item-element/index.tsx index 7bc78ab667..4e49bbbe8d 100644 --- a/src/button-dropdown/item-element/index.tsx +++ b/src/button-dropdown/item-element/index.tsx @@ -136,6 +136,7 @@ function MenuItem({ item, disabled, highlighted, linkStyle }: MenuItemProps) {