Skip to content

Commit

Permalink
feat: Support for toggle buttons in button group
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-kot committed Oct 30, 2024
1 parent 6ba69fa commit 421b593
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 30 deletions.
12 changes: 11 additions & 1 deletion pages/button-group/item-permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import createPermutations from '../utils/permutations';
import PermutationsView from '../utils/permutations-view';
import ScreenshotArea from '../utils/screenshot-area';

const itemPermutations = createPermutations<ButtonGroupProps.IconButton>([
const itemPermutations = createPermutations<ButtonGroupProps.Item>([
// Undefined icon
{
type: ['icon-button'],
Expand Down Expand Up @@ -42,6 +42,16 @@ const itemPermutations = createPermutations<ButtonGroupProps.IconButton>([
</StatusIndicator>,
],
},
// Toggle button
{
type: ['icon-toggle-button'],
id: ['test'],
iconName: ['star'],
pressedIconName: ['star-filled'],
text: ['Add to favorites'],
pressedText: ['Added to favorites'],
pressed: [false, true],
},
]);

const menuDropdownPermutations = createPermutations<ButtonGroupProps.MenuDropdown>([
Expand Down
10 changes: 8 additions & 2 deletions pages/button-group/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ const feedbackGroup: ButtonGroupProps.Group = {
text: 'Vote',
items: [
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'like',
iconName: 'thumbs-up',
pressedIconName: 'thumbs-up-filled',
text: 'Like',
pressedText: 'Like',
pressed: true,
},
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'dislike',
iconName: 'thumbs-down',
pressedIconName: 'thumbs-down-filled',
text: 'Dislike',
pressedText: 'Dislike',
pressed: false,
},
],
};
Expand Down
26 changes: 17 additions & 9 deletions pages/button-group/test.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@ export default function ButtonGroupPage() {
text: 'Vote',
items: [
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'like',
iconName: feedback === 'like' ? 'thumbs-up-filled' : 'thumbs-up',
iconName: 'thumbs-up',
pressedIconName: 'thumbs-up-filled',
text: 'Like',
pressedText: 'Liked',
pressed: feedback === 'like',
},
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'dislike',
iconName: feedback === 'dislike' ? 'thumbs-down-filled' : 'thumbs-down',
iconName: 'thumbs-down',
pressedIconName: 'thumbs-down-filled',
text: 'Dislike',
pressedText: 'Disliked',
pressed: feedback === 'dislike',
},
],
};
Expand All @@ -55,12 +61,14 @@ export default function ButtonGroupPage() {
text: 'Favorite',
items: [
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'favorite',
iconName: isFavorite ? 'star-filled' : 'star',
iconName: 'star',
pressedIconName: 'star-filled',
text: 'Add to favorites',
pressedText: 'Added to favorites',
loading: loadingId === 'favorite',
popoverFeedback: loadingId === 'favorite' ? '...' : isFavorite ? 'Set as favorite' : 'Removed',
pressed: isFavorite,
},
],
};
Expand Down Expand Up @@ -191,9 +199,9 @@ export default function ButtonGroupPage() {
switch (detail.id) {
case 'like':
case 'dislike':
return syncAction(() => setFeedback(prev => (prev !== detail.id ? (detail.id as 'like' | 'dislike') : 'none')));
return syncAction(() => setFeedback(detail.pressed ? (detail.id as 'like' | 'dislike') : 'none'));
case 'favorite':
return asyncAction(() => setFavorite(prev => !prev));
return asyncAction(() => setFavorite(!!detail.pressed));
case 'send':
return syncAction(() => setCanSend(false));
case 'redo':
Expand Down
33 changes: 28 additions & 5 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3795,6 +3795,11 @@ exports[`Documenter definition for button-group matches the snapshot: button-gro
"optional": false,
"type": "string",
},
{
"name": "pressed",
"optional": true,
"type": "false | true",
},
],
"type": "object",
},
Expand Down Expand Up @@ -3861,15 +3866,33 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
### icon-button

* \`id\` (string) - The unique identifier of the button, used as detail in \`onItemClick\` handler and to focus the button using \`ref.focus(id)\`.
* \`text\` (string) - The name shown as a tooltip or menu text for this button.
* \`disabled\` (optional, boolean) - The disabled state indication for the button.
* \`loading\` (optional, boolean) - The loading state indication for the button.
* \`text\` (string) - The name shown as a tooltip for this button.
* \`disabled\` (optional, boolean) - The disabled state indication for this button.
* \`loading\` (optional, boolean) - The loading state indication for this 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/).
* \`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/).
* \`popoverFeedback\` (optional, string) - Text that appears when the user clicks the button. Use to provide feedback to the user.
* \`popoverFeedback\` (optional, ReactNode) - Text that appears when the user clicks the button. Use to provide feedback to the user.

### icon-toggle-button

* \`id\` (string) - The unique identifier of the button, used as detail in \`onItemClick\` handler and to focus the button using \`ref.focus(id)\`.
* \`pressed\` (boolean) - The toggle button pressed state.
* \`text\` (string) - The name shown as a tooltip for this button.
* \`pressedText\` (string) - The name shown as a tooltip for this button in pressed state.
* \`disabled\` (optional, boolean) - The disabled state indication for this button.
* \`loading\` (optional, boolean) - The loading state indication for this 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/).
* \`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/).
* \`pressedIconName\` (optional, string) - Specifies the name of the icon in pressed state, used with the [icon component](/components/icon/).
* \`pressedIconUrl\` (optional, string) - Specifies the URL of a custom icon in pressed state.
* \`pressedIconSvg\` (optional, ReactNode) - Custom SVG icon in pressed state. Equivalent to the \`svg\` slot of the [icon component](/components/icon/).
* \`popoverFeedback\` (optional, ReactNode) - Text that appears when the user clicks the button. Use to provide feedback to the user.
* \`pressedPopoverFeedback\` (optional, ReactNode) - Text that appears when the user clicks the button in pressed state. Defaults to \`popoverFeedback\`.

### menu-dropdown

Expand All @@ -3880,7 +3903,7 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
* \`loadingText\` (optional, string) - The loading text announced to screen readers.
* \`items\` (ButtonDropdownProps.ItemOrGroup[]) - The array of dropdown items that belong to this menu.

group
### group

* \`text\` (string) - The name of the group rendered as ARIA label for this group.
* \`items\` ((ButtonGroupProps.IconButton | ButtonGroupProps.MenuDropdown)[]) - The array of items that belong to this group.
Expand Down
36 changes: 30 additions & 6 deletions src/button-group/__tests__/button-group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,23 @@ const items: ButtonGroupProps.ItemOrGroup[] = [
type: 'group',
text: 'Feedback',
items: [
{ type: 'icon-button', id: 'like', text: 'Like', iconName: 'thumbs-up', popoverFeedback: 'Liked' },
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'like',
pressed: false,
text: 'Like',
pressedText: 'Liked',
iconName: 'thumbs-up',
pressedIconName: 'thumbs-up-filled',
},
{
type: 'icon-toggle-button',
id: 'dislike',
disabled: true,
text: 'dislike',
pressed: false,
text: 'Dislike',
pressedText: 'Disliked',
iconName: 'thumbs-down',
popoverFeedback: 'Disliked',
pressedIconName: 'thumbs-down-filled',
},
],
},
Expand All @@ -64,6 +73,18 @@ const items: ButtonGroupProps.ItemOrGroup[] = [
},
];

function traverseItems(
items: readonly ButtonGroupProps.ItemOrGroup[],
onItem: (item: ButtonGroupProps.Item) => ButtonGroupProps.Item
): readonly ButtonGroupProps.ItemOrGroup[] {
return items.map(itemOrGroup => {
if (itemOrGroup.type === 'group') {
return { ...itemOrGroup, items: traverseItems(itemOrGroup.items, onItem) } as ButtonGroupProps.Group;
}
return onItem(itemOrGroup);
});
}

const emptyGroup: ButtonGroupProps.ItemOrGroup[] = [
{
type: 'group',
Expand Down Expand Up @@ -114,7 +135,10 @@ describe('focus', () => {

test('focuses the correct item with keyboard', () => {
const ref: { current: ButtonGroupProps.Ref | null } = { current: null };
const { wrapper } = renderButtonGroup({ items }, ref);
const itemsWithDisabledDislike = traverseItems(items, item =>
item.id === 'dislike' ? { ...item, disabled: true } : item
);
const { wrapper } = renderButtonGroup({ items: itemsWithDisabledDislike }, ref);
ref.current?.focus('copy');

fireEvent.keyDown(wrapper.getElement(), { keyCode: KeyCode.right });
Expand Down
87 changes: 87 additions & 0 deletions src/button-group/icon-toggle-button-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { forwardRef } from 'react';
import clsx from 'clsx';

import { warnOnce } from '@cloudscape-design/component-toolkit/internal';

import { ButtonProps } from '../button/interfaces.js';
import Tooltip from '../internal/components/tooltip/index.js';
import { CancelableEventHandler, fireCancelableEvent } from '../internal/events/index.js';
import InternalLiveRegion from '../live-region/internal.js';
import { InternalToggleButton } from '../toggle-button/internal.js';
import { ButtonGroupProps } from './interfaces.js';

import testUtilStyles from './test-classes/styles.css.js';

const IconToggleButtonItem = forwardRef(
(
{
item,
showTooltip,
showFeedback,
onItemClick,
}: {
item: ButtonGroupProps.IconToggleButton;
showTooltip: boolean;
showFeedback: boolean;
onItemClick?: CancelableEventHandler<ButtonGroupProps.ItemClickDetails>;
},
ref: React.Ref<ButtonProps.Ref>
) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const hasIcon = item.iconName || item.iconUrl || item.iconSvg;
const hasPressedIcon = item.pressedIconName || item.pressedIconUrl || item.pressedIconSvg;

if (!hasIcon) {
warnOnce('ButtonGroup', `Missing icon for item with id: ${item.id}`);

Check warning on line 37 in src/button-group/icon-toggle-button-item.tsx

View check run for this annotation

Codecov / codecov/patch

src/button-group/icon-toggle-button-item.tsx#L37

Added line #L37 was not covered by tests
}
if (!hasPressedIcon) {
warnOnce('ButtonGroup', `Missing pressed icon for item with id: ${item.id}`);

Check warning on line 40 in src/button-group/icon-toggle-button-item.tsx

View check run for this annotation

Codecov / codecov/patch

src/button-group/icon-toggle-button-item.tsx#L40

Added line #L40 was not covered by tests
}

const tooltipContent = item.pressed ? item.pressedText : item.text;
const feedbackContent = item.pressed ? item.pressedPopoverFeedback ?? item.popoverFeedback : item.popoverFeedback;
const canShowTooltip = showTooltip && !item.disabled && !item.loading;
const canShowFeedback = showTooltip && showFeedback && feedbackContent;
return (
<div ref={containerRef}>
<InternalToggleButton
variant="icon"
pressed={item.pressed}
loading={item.loading}
loadingText={item.loadingText}
disabled={item.disabled}
iconName={hasIcon ? item.iconName : 'close'}
iconUrl={item.iconUrl}
iconSvg={item.iconSvg}
pressedIconName={hasIcon ? item.pressedIconName : 'close'}
pressedIconUrl={item.pressedIconUrl}
pressedIconSvg={item.pressedIconUrl}
ariaLabel={tooltipContent}
onChange={event => fireCancelableEvent(onItemClick, { id: item.id, pressed: event.detail.pressed })}

Check warning on line 62 in src/button-group/icon-toggle-button-item.tsx

View check run for this annotation

Codecov / codecov/patch

src/button-group/icon-toggle-button-item.tsx#L62

Added line #L62 was not covered by tests
ref={ref}
data-testid={item.id}
data-itemid={item.id}
className={clsx(testUtilStyles.item, testUtilStyles['button-group-item'])}
__title=""
>
{tooltipContent}
</InternalToggleButton>
{(canShowTooltip || canShowFeedback) && (
<Tooltip
trackRef={containerRef}
trackKey={item.id}
value={
(showFeedback && <InternalLiveRegion tagName="span">{feedbackContent}</InternalLiveRegion>) ||
tooltipContent
}
className={clsx(testUtilStyles.tooltip, testUtilStyles['button-group-tooltip'])}
/>
)}
</div>
);
}
);

export default IconToggleButtonItem;
Loading

0 comments on commit 421b593

Please sign in to comment.