Skip to content
Merged
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
13 changes: 13 additions & 0 deletions pages/button-dropdown/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ const permutations = createPermutations<ButtonDropdownProps>([
ariaLabel: ['Button dropdown'],
fullWidth: [false, true],
},
// Custom trigger icon
{
items: [[]],
variant: ['icon', 'inline-icon'],
ariaLabel: ['Button dropdown'],
iconName: [undefined, 'settings'],
iconSvg: [
undefined,
<svg key="custom-icon" focusable="false" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" stroke="currentColor" fill="none" strokeWidth="2" />
</svg>,
],
},
]);

export default function () {
Expand Down
22 changes: 22 additions & 0 deletions pages/button-group/item-permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ const menuDropdownPermutations = createPermutations<ButtonGroupProps.MenuDropdow
],
],
},
// Custom icon
{
type: ['menu-dropdown'],
id: ['more-actions'],
text: ['More actions'],
iconName: [undefined, 'settings'],
iconSvg: [
undefined,
<svg key="custom-icon" focusable="false" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" stroke="currentColor" fill="none" strokeWidth="2" />
</svg>,
],
items: [
[
{
id: 'cut',
iconName: 'delete-marker',
text: 'Cut',
},
],
],
},
]);

export default function () {
Expand Down
178 changes: 178 additions & 0 deletions src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6113,6 +6113,172 @@ because fixed positioning results in a slight, visible lag when scrolling comple
"optional": true,
"type": "boolean",
},
{
"description": "Specifies alternate text for a custom icon, for use with \`iconUrl\`. Applies to the \`icon\` and \`inline-icon\` variants only.",
"name": "iconAlt",
"optional": true,
"type": "string",
},
{
"description": "Specifies the name of the icon used in the button dropdown trigger, used with the [icon component](/components/icon/).
Defaults to \`ellipsis\`. Applies to the \`icon\` and \`inline-icon\` variants only.",
"inlineType": {
"name": "IconProps.Name",
"type": "union",
"values": [
"search",
"map",
"filter",
"key",
"file",
"pause",
"play",
"microphone",
"remove",
"copy",
"menu",
"script",
"close",
"status-pending",
"refresh",
"external",
"history",
"group",
"calendar",
"ellipsis",
"zoom-in",
"zoom-out",
"dot",
"security",
"download",
"edit",
"add-plus",
"anchor-link",
"angle-left-double",
"angle-left",
"angle-right-double",
"angle-right",
"angle-up",
"angle-down",
"announcement",
"arrow-left",
"arrow-right",
"arrow-up",
"arrow-down",
"at-symbol",
"audio-full",
"audio-half",
"audio-off",
"backward-10-seconds",
"bug",
"call",
"caret-down-filled",
"caret-down",
"caret-left-filled",
"caret-right-filled",
"caret-up-filled",
"caret-up",
"check",
"contact",
"closed-caption",
"closed-caption-unavailable",
"command-prompt",
"delete-marker",
"drag-indicator",
"edit-gen-ai",
"envelope",
"exit-full-screen",
"expand",
"face-happy",
"face-happy-filled",
"face-neutral",
"face-neutral-filled",
"face-sad",
"face-sad-filled",
"file-open",
"flag",
"folder-open",
"folder",
"forward-10-seconds",
"full-screen",
"gen-ai",
"globe",
"grid-view",
"group-active",
"heart",
"heart-filled",
"insert-row",
"keyboard",
"light-dark",
"list-view",
"location-pin",
"lock-private",
"microphone-off",
"mini-player",
"multiscreen",
"notification",
"redo",
"resize-area",
"search-gen-ai",
"settings",
"send",
"share",
"shrink",
"slash",
"slash-divider",
"star-filled",
"star-half",
"star",
"status-in-progress",
"status-info",
"status-negative",
"status-not-started",
"status-positive",
"status-stopped",
"status-warning",
"stop-circle",
"subtract-minus",
"suggestions",
"suggestions-gen-ai",
"support",
"thumbs-down-filled",
"thumbs-down",
"thumbs-up-filled",
"thumbs-up",
"ticket",
"transcript",
"treeview-collapse",
"treeview-expand",
"undo",
"unlocked",
"upload-download",
"upload",
"user-profile-active",
"user-profile",
"video-off",
"video-on",
"video-unavailable",
"video-camera-off",
"video-camera-on",
"video-camera-unavailable",
"view-full",
"view-horizontal",
"view-vertical",
"zoom-to-fit",
],
},
"name": "iconName",
"optional": true,
"type": "string",
},
{
"description": "Specifies the URL of a custom icon. Applies to the \`icon\` and \`inline-icon\` variants only.

If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence.",
"name": "iconUrl",
"optional": true,
"type": "string",
},
{
"deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases,
use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must
Expand Down Expand Up @@ -6625,6 +6791,14 @@ When returning \`null\`, the default styling will be applied.",
"isDefault": true,
"name": "children",
},
{
"description": "Custom SVG icon. Equivalent to the \`svg\` slot of the [icon component](/components/icon/).
Applies to the \`icon\` and \`inline-icon\` variants only.

If you set both \`iconUrl\` and \`iconSvg\`, \`iconSvg\` will take precedence.",
"isDefault": false,
"name": "iconSvg",
},
],
"releaseStatus": "stable",
}
Expand Down Expand Up @@ -6789,6 +6963,10 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
* \`disabledReason\` (optional, boolean) - Provides a reason why the button is disabled (only when \`disabled\` is \`true\`). If provided, the button becomes focusable.
* \`loading\` (optional, boolean) - The loading state indication for the menu button.
* \`loadingText\` (optional, string) - The loading text announced to screen readers.
* \`iconName\` (optional, string) - Specifies the name of the icon, used with the [icon component](/components/icon/). Defaults to \`ellipsis\`.
* \`iconAlt\` (optional, string) - Specifies alternate text for the icon when using \`iconUrl\`.
* \`iconUrl\` (optional, string) - Specifies the URL of a custom icon.
* \`iconSvg\` (optional, ReactNode) - Custom SVG icon. Equivalent to the \`svg\` slot of the [icon component](/components/icon/).
* \`items\` (ButtonDropdownProps.ItemOrGroup[]) - The array of dropdown items that belong to this menu.

### group
Expand Down
63 changes: 63 additions & 0 deletions src/button-dropdown/__tests__/button-dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ButtonDropdown, { ButtonDropdownProps } from '../../../lib/components/but
import InternalButtonDropdown from '../../../lib/components/button-dropdown/internal';
import { KeyCode } from '../../../lib/components/internal/keycode';
import createWrapper, { ButtonWrapper } from '../../../lib/components/test-utils/dom';
import { getIconHTML } from '../../icon/__tests__/utils';

import iconStyles from '../../../lib/components/icon/styles.css.js';
import liveRegionStyles from '../../../lib/components/live-region/test-classes/styles.css.js';
Expand Down Expand Up @@ -539,3 +540,65 @@ describe('native attributes', () => {
expect(wrapper.findMainAction()?.getElement()).toHaveAttribute('aria-describedby', 'my-custom-description');
});
});

describe('custom trigger icon', () => {
(['icon', 'inline-icon'] as Array<ButtonDropdownProps['variant']>).forEach(variant => {
describe(`"${variant}" variant`, () => {
test('renders ellipsis icon by default', () => {
const trigger = renderButtonDropdown({ items, variant, ariaLabel: 'Actions' }).findNativeButton();
const icon = trigger.findIcon()!.find('svg')!;

expect(icon.getElement()).toContainHTML(getIconHTML('ellipsis'));
});

test('renders custom icon with iconName', () => {
const trigger = renderButtonDropdown({
items,
variant,
ariaLabel: 'Actions',
iconName: 'settings',
}).findNativeButton();
const icon = trigger.findIcon()!.find('svg')!;

expect(icon.getElement()).toContainHTML(getIconHTML('settings'));
});

test('renders custom icon with iconUrl', () => {
const iconUrl = 'data:image/png;base64,aaaa';
const wrapper = renderButtonDropdown({ items, variant, ariaLabel: 'Actions', iconUrl, iconAlt: 'Custom icon' });

const icon = wrapper.findNativeButton().find('img')!.getElement();
expect(icon).toHaveAttribute('src', iconUrl);
expect(icon).toHaveAttribute('alt', 'Custom icon');
});

test('renders custom icon with iconSvg', () => {
const iconSvg = (
<svg className="custom-trigger-icon" focusable="false" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7" />
</svg>
);
const wrapper = renderButtonDropdown({ items, variant, ariaLabel: 'Actions', iconSvg });

expect(wrapper.findNativeButton().find('.custom-trigger-icon')).toBeTruthy();
});

test('does not warn when custom icon is provided', () => {
renderButtonDropdown({ items, variant, ariaLabel: 'Actions', iconName: 'settings' });
expect(warnOnce).not.toHaveBeenCalled();
});
});
});

(['normal', 'primary'] as Array<ButtonDropdownProps['variant']>).forEach(variant => {
test(`warns and ignores custom icon for "${variant}" variant`, () => {
const wrapper = renderButtonDropdown({ items, variant, children: 'Actions', iconName: 'settings' });

expect(warnOnce).toHaveBeenCalledWith(
'ButtonDropdown',
'Custom icon is only supported for "icon" and "inline-icon" component variant.'
);
expect(wrapper.findNativeButton().getElement()).toHaveTextContent('Actions');
});
});
});
11 changes: 10 additions & 1 deletion src/button-dropdown/index.tsx
Comment thread
michaeldowseza marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const ButtonDropdown = React.forwardRef(
expandableGroups = false,
expandToViewport = false,
ariaLabel,
iconName,
iconAlt,
iconUrl,
iconSvg,
children,
onItemClick,
onItemFollow,
Expand All @@ -42,10 +46,11 @@ const ButtonDropdown = React.forwardRef(
ref: React.Ref<ButtonDropdownProps.Ref>
) => {
const baseComponentProps = useBaseComponent('ButtonDropdown', {
props: { expandToViewport, expandableGroups, variant },
props: { expandToViewport, expandableGroups, variant, iconName },
metadata: {
mainAction: !!mainAction,
checkboxItems: hasCheckboxItems(items),
hasCustomIcon: Boolean(iconUrl || iconSvg || iconName),
hasDisabledReason: Boolean(disabledReason),
hasDisabledReasons: hasDisabledReasonItems(items),
},
Expand Down Expand Up @@ -73,6 +78,10 @@ const ButtonDropdown = React.forwardRef(
expandableGroups={expandableGroups}
expandToViewport={expandToViewport}
ariaLabel={ariaLabel}
iconName={iconName}
iconAlt={iconAlt}
iconUrl={iconUrl}
iconSvg={iconSvg}
onItemClick={onItemClick}
onItemFollow={onItemFollow}
mainAction={mainAction}
Expand Down
22 changes: 22 additions & 0 deletions src/button-dropdown/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,28 @@ export interface ButtonDropdownProps extends BaseComponentProps, ExpandToViewpor
* * `inline-icon` for icon buttons with no outer padding
*/
variant?: ButtonDropdownProps.Variant;
/**
* Specifies the name of the icon used in the button dropdown trigger, used with the [icon component](/components/icon/).
* Defaults to `ellipsis`. Applies to the `icon` and `inline-icon` variants only.
*/
iconName?: IconProps.Name;
/**
* Specifies alternate text for a custom icon, for use with `iconUrl`. Applies to the `icon` and `inline-icon` variants only.
*/
iconAlt?: string;
/**
* Specifies the URL of a custom icon. Applies to the `icon` and `inline-icon` variants only.
*
* If you set both `iconUrl` and `iconSvg`, `iconSvg` will take precedence.
*/
iconUrl?: string;
/**
* Custom SVG icon. Equivalent to the `svg` slot of the [icon component](/components/icon/).
* Applies to the `icon` and `inline-icon` variants only.
*
* If you set both `iconUrl` and `iconSvg`, `iconSvg` will take precedence.
*/
iconSvg?: React.ReactNode;
/**
* Controls expandability of the item groups.
*/
Expand Down
Loading
Loading