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 21, 2024
1 parent 4014103 commit 883bd49
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 12 deletions.
44 changes: 40 additions & 4 deletions pages/button-group/permutations.page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import React, { useState } from 'react';
import cloneDeep from 'lodash/cloneDeep';

import ButtonGroup, { ButtonGroupProps } from '~components/button-group';

Expand All @@ -14,16 +15,20 @@ 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',
pressed: true,
},
{
type: 'icon-button',
type: 'icon-toggle-button',
id: 'dislike',
iconName: 'thumbs-down',
pressedIconName: 'thumbs-down-filled',
text: 'Dislike',
pressed: false,
},
],
};
Expand Down Expand Up @@ -98,9 +103,40 @@ export default function () {
<h1>ButtonGroup permutations</h1>
<PermutationsView
permutations={buttonGroupPermutations}
render={permutation => <div>{<ButtonGroup {...permutation} />}</div>}
render={permutation => <div>{<StatefulButtonGroup {...permutation} />}</div>}
/>
</article>
</ScreenshotArea>
);
}

function StatefulButtonGroup(props: ButtonGroupProps) {
const [feedback, setFeedback] = useState<'like' | 'dislike'>('like');
return (
<ButtonGroup
{...props}
items={withFeedbackState(props.items, feedback)}
onItemClick={({ detail }) => {
switch (detail.id) {
case 'like':
return setFeedback(detail.pressed ? 'like' : 'dislike');
case 'dislike':
return setFeedback(detail.pressed ? 'dislike' : 'like');
default:
// not implemented
}
}}
/>
);
}

function withFeedbackState(source: readonly ButtonGroupProps.ItemOrGroup[], feedback: 'like' | 'dislike') {
const clone = cloneDeep(source);
for (const itemOrGroup of clone) {
if (itemOrGroup.type === 'group' && itemOrGroup.text === 'Vote') {
itemOrGroup.items[0].type === 'icon-toggle-button' && (itemOrGroup.items[0].pressed = feedback === 'like');
itemOrGroup.items[1].type === 'icon-toggle-button' && (itemOrGroup.items[1].pressed = feedback === 'dislike');
}
}
return clone;
}
9 changes: 5 additions & 4 deletions src/button-group/icon-button-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ButtonProps } from '../button/interfaces.js';
import { InternalButton } from '../button/internal.js';
import LiveRegion from '../internal/components/live-region/index.js';
import Tooltip from '../internal/components/tooltip/index.js';
import { CancelableEventHandler, ClickDetail } from '../internal/events/index.js';
import { CancelableEventHandler, fireCancelableEvent } from '../internal/events/index.js';
import { ButtonGroupProps } from './interfaces.js';

import testUtilStyles from './test-classes/styles.css.js';
Expand All @@ -25,7 +25,7 @@ const IconButtonItem = forwardRef(
item: ButtonGroupProps.IconButton;
showTooltip: boolean;
showFeedback: boolean;
onItemClick?: CancelableEventHandler<ClickDetail>;
onItemClick?: CancelableEventHandler<ButtonGroupProps.ItemClickDetails>;
},
ref: React.Ref<ButtonProps.Ref>
) => {
Expand All @@ -44,10 +44,11 @@ const IconButtonItem = forwardRef(
loadingText={item.loadingText}
disabled={item.disabled}
iconName={hasIcon ? item.iconName : 'close'}
iconAlt={item.text}
iconUrl={item.iconUrl}
iconSvg={item.iconSvg}
iconAlt={item.text}
ariaLabel={item.text}
onClick={onItemClick}
onClick={event => fireCancelableEvent(onItemClick, { id: item.id }, event)}
ref={ref}
data-testid={item.id}
data-itemid={item.id}
Expand Down
76 changes: 76 additions & 0 deletions src/button-group/icon-toggle-button-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 { InternalToggleButton } from '../toggle-button/internal.js';
import { ButtonGroupProps } from './interfaces.js';

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

const IconToggleButtonItem = forwardRef(
(
{
item,
showTooltip,
onItemClick,
}: {
item: ButtonGroupProps.IconToggleButton;
showTooltip: boolean;
onItemClick?: CancelableEventHandler<ButtonGroupProps.ItemClickDetails>;
},
ref: React.Ref<ButtonProps.Ref>
) => {
const containerRef = React.useRef<HTMLDivElement>(null);

Check warning on line 29 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#L28-L29

Added lines #L28 - L29 were not covered by tests
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 34 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#L34

Added line #L34 was not covered by tests
}
if (!hasPressedIcon) {
warnOnce('ButtonGroup', `Missing pressed 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
}

return (

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
<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={item.text}
onChange={event => fireCancelableEvent(onItemClick, { id: item.id, pressed: event.detail.pressed })}

Check warning on line 55 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#L55

Added line #L55 was not covered by tests
ref={ref}
data-testid={item.id}
data-itemid={item.id}
className={clsx(testUtilStyles.item, testUtilStyles['button-group-item'])}
>
{item.text}
</InternalToggleButton>
{showTooltip && !item.disabled && !item.loading && (
<Tooltip

Check warning on line 64 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#L64

Added line #L64 was not covered by tests
trackRef={containerRef}
trackKey={item.id}
value={item.text}
className={clsx(testUtilStyles.tooltip, testUtilStyles['button-group-tooltip'])}
/>
)}
</div>
);
}
);

export default IconToggleButtonItem;
34 changes: 33 additions & 1 deletion src/button-group/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ export interface ButtonGroupProps extends BaseComponentProps {
* * `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.
*
* * ### 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)`.
* * `text` (string) - The name shown as a tooltip or menu text for this button.
* * `pressed` (boolean) - The toggle button pressed state.
* * `disabled` (optional, boolean) - The disabled state indication for the button.
* * `loading` (optional, boolean) - The loading state indication for the 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/).
*
* ### menu-dropdown
*
* * `id` (string) - The unique identifier of the button, used as detail in `onItemClick`.
Expand Down Expand Up @@ -75,7 +90,7 @@ export namespace ButtonGroupProps {
export type Variant = 'icon';

export type ItemOrGroup = Item | Group;
export type Item = IconButton | MenuDropdown;
export type Item = IconButton | IconToggleButton | MenuDropdown;

export interface IconButton {
type: 'icon-button';
Expand All @@ -91,6 +106,22 @@ export namespace ButtonGroupProps {
popoverFeedback?: React.ReactNode;
}

export interface IconToggleButton {
type: 'icon-toggle-button';
id: string;
text: string;
pressed: boolean;
disabled?: boolean;
loading?: boolean;
loadingText?: string;
iconName?: IconProps.Name;
iconUrl?: string;
iconSvg?: React.ReactNode;
pressedIconName?: IconProps.Name;
pressedIconUrl?: string;
pressedIconSvg?: React.ReactNode;
}

export interface MenuDropdown {
type: 'menu-dropdown';
id: string;
Expand All @@ -109,6 +140,7 @@ export namespace ButtonGroupProps {

export interface ItemClickDetails {
id: string;
pressed?: boolean;
}

export interface Ref {
Expand Down
15 changes: 12 additions & 3 deletions src/button-group/item-element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';

import { ButtonProps } from '../button/interfaces.js';
import { ClickDetail, fireCancelableEvent, NonCancelableEventHandler } from '../internal/events';
import { fireCancelableEvent, NonCancelableEventHandler } from '../internal/events';
import { nodeBelongs } from '../internal/utils/node-belongs';
import IconButtonItem from './icon-button-item';
import IconToggleButtonItem from './icon-toggle-button-item.js';
import { ButtonGroupProps } from './interfaces';
import MenuDropdownItem from './menu-dropdown-item';

Expand Down Expand Up @@ -79,14 +80,14 @@ const ItemElement = forwardRef(
setTooltip(show ? { item: item.id, feedback: false } : null);
};

const onClickHandler = (event: CustomEvent<ButtonGroupProps.ItemClickDetails | ClickDetail>) => {
const onClickHandler = (event: CustomEvent<ButtonGroupProps.ItemClickDetails>) => {
const hasPopoverFeedback = 'popoverFeedback' in item && item.popoverFeedback;

if (hasPopoverFeedback) {
setTooltip({ item: item.id, feedback: true });
}

fireCancelableEvent(onItemClick, { id: 'id' in event.detail ? event.detail.id : item.id }, event);
fireCancelableEvent(onItemClick, event.detail, event);
};

return (
Expand Down Expand Up @@ -115,6 +116,14 @@ const ItemElement = forwardRef(
showFeedback={!!tooltip?.feedback}
/>
)}
{item.type === 'icon-toggle-button' && (
<IconToggleButtonItem

Check warning on line 120 in src/button-group/item-element.tsx

View check run for this annotation

Codecov / codecov/patch

src/button-group/item-element.tsx#L120

Added line #L120 was not covered by tests
ref={buttonRef}
item={item}
onItemClick={onClickHandler}
showTooltip={tooltip?.item === item.id}
/>
)}
{item.type === 'menu-dropdown' && (
<MenuDropdownItem
ref={buttonRef}
Expand Down

0 comments on commit 883bd49

Please sign in to comment.