Skip to content

Commit 6012a00

Browse files
authored
feat: Add hide add-button, additional actions slot to attribute-editor (#4014)
1 parent 7790068 commit 6012a00

File tree

6 files changed

+121
-19
lines changed

6 files changed

+121
-19
lines changed

pages/attribute-editor/permutations.page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44

55
import AttributeEditor, { AttributeEditorProps } from '~components/attribute-editor';
66
import Box from '~components/box';
7+
import Button from '~components/button';
78
import Input from '~components/input';
89
import StatusIndicator from '~components/status-indicator';
910

@@ -144,6 +145,41 @@ export const permutations = createPermutations<AttributeEditorProps<Item>>([
144145
addButtonText: ['Add item'],
145146
removeButtonText: ['Remove item'],
146147
},
148+
{
149+
definition: [definition2],
150+
items: [defaultItems],
151+
addButtonText: ['Add item'],
152+
removeButtonText: ['Remove item'],
153+
additionalActions: [
154+
<Button key="test-button" variant="normal">
155+
Test Button
156+
</Button>,
157+
[
158+
<Button key="import" variant="normal">
159+
Import
160+
</Button>,
161+
<Button key="export" variant="normal">
162+
Export
163+
</Button>,
164+
<Button key="long-text" variant="normal">
165+
A very long button text to try out wrapping to a second row
166+
</Button>,
167+
],
168+
],
169+
},
170+
{
171+
definition: [definition2],
172+
items: [defaultItems],
173+
removeButtonText: ['Remove item'],
174+
hideAddButton: [true],
175+
addButtonText: ['Add item'],
176+
additionalActions: [
177+
<Button key="add-custom" variant="primary">
178+
Add custom item
179+
</Button>,
180+
undefined,
181+
],
182+
},
147183
]);
148184

149185
export default function AttributeEditorPermutations() {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,6 +2614,12 @@ A \`gridLayout\` is an array of breakpoint definitions. Each definition consists
26142614
"optional": true,
26152615
"type": "ReadonlyArray<AttributeEditorProps.GridLayout>",
26162616
},
2617+
{
2618+
"description": "Determines whether the add button is hidden",
2619+
"name": "hideAddButton",
2620+
"optional": true,
2621+
"type": "boolean",
2622+
},
26172623
{
26182624
"description": "An object containing all the necessary localized strings required by the component.",
26192625
"i18nTag": true,
@@ -2721,6 +2727,11 @@ The display of a row is handled by the \`definition\` property.",
27212727
},
27222728
],
27232729
"regions": [
2730+
{
2731+
"description": "Specifies additional actions displayed next to the add-button (or instead of the add-button if hidden).",
2732+
"isDefault": false,
2733+
"name": "additionalActions",
2734+
},
27242735
{
27252736
"description": "Displayed below the add button. Use it for additional information related to the attribute editor.",
27262737
"isDefault": false,

src/attribute-editor/__tests__/attribute-editor.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { render, waitFor } from '@testing-library/react';
66
import { useContainerQuery } from '@cloudscape-design/component-toolkit';
77

88
import AttributeEditor, { AttributeEditorProps } from '../../../lib/components/attribute-editor';
9+
import Button from '../../../lib/components/button';
910
import ButtonDropdown from '../../../lib/components/button-dropdown';
1011
import TestI18nProvider from '../../../lib/components/i18n/testing';
1112
import Input from '../../../lib/components/input';
@@ -209,6 +210,21 @@ describe('Attribute Editor', () => {
209210
expect(wrapper.findAddButton().getElement()).toHaveFocus();
210211
});
211212

213+
test('hides add button when hideAddButton is true', () => {
214+
const wrapper = renderAttributeEditor({ hideAddButton: true });
215+
expect(wrapper.findAddButton()).toBeNull();
216+
});
217+
218+
test('shows add button when hideAddButton is false', () => {
219+
const wrapper = renderAttributeEditor({ hideAddButton: false });
220+
expect(wrapper.findAddButton()).not.toBeNull();
221+
});
222+
223+
test('shows add button by default when hideAddButton is not specified', () => {
224+
const wrapper = renderAttributeEditor();
225+
expect(wrapper.findAddButton()).not.toBeNull();
226+
});
227+
212228
test('has no aria-describedby if there is no additional info', () => {
213229
const wrapper = renderAttributeEditor({ ...defaultProps });
214230
const buttonElement = wrapper.findAddButton().getElement();
@@ -652,6 +668,16 @@ describe('Attribute Editor', () => {
652668
});
653669
});
654670

671+
describe('additional actions', () => {
672+
test('renders additional actions alongside add button if provided', () => {
673+
const additionalActions = <Button data-testid="additional-test-button">Additional Action Button</Button>;
674+
const { container } = render(<AttributeEditor {...defaultProps} additionalActions={additionalActions} />);
675+
const wrapper = createWrapper(container).findAttributeEditor()!;
676+
expect(wrapper.getElement().querySelector('[data-testid="additional-test-button"]')).not.toBeNull();
677+
expect(wrapper.findAddButton()).not.toBeNull();
678+
});
679+
});
680+
655681
describe('custom buttons', () => {
656682
test('allows a custom row action', () => {
657683
const { container } = render(

src/attribute-editor/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import InternalAttributeEditor from './internal';
1010

1111
const AttributeEditor = React.forwardRef(
1212
<T,>(
13-
{ items = [], addButtonVariant = 'normal', isItemRemovable = () => true, ...props }: AttributeEditorProps<T>,
13+
{
14+
items = [],
15+
addButtonVariant = 'normal',
16+
isItemRemovable = () => true,
17+
hideAddButton,
18+
additionalActions,
19+
...props
20+
}: AttributeEditorProps<T>,
1421
ref: React.Ref<AttributeEditorProps.Ref>
1522
) => {
1623
const baseComponentProps = useBaseComponent('AttributeEditor', {
@@ -28,6 +35,8 @@ const AttributeEditor = React.forwardRef(
2835
items={items}
2936
isItemRemovable={isItemRemovable}
3037
addButtonVariant={addButtonVariant}
38+
additionalActions={additionalActions}
39+
hideAddButton={hideAddButton}
3140
{...props}
3241
{...baseComponentProps}
3342
ref={ref}

src/attribute-editor/interfaces.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ export interface AttributeEditorProps<T> extends BaseComponentProps {
142142
*/
143143
disableAddButton?: boolean;
144144

145+
/**
146+
* Determines whether the add button is hidden
147+
*/
148+
hideAddButton?: boolean;
149+
150+
/**
151+
* Specifies additional actions displayed next to the add-button (or instead of the add-button if hidden).
152+
*/
153+
additionalActions?: React.ReactNode;
154+
145155
/**
146156
* Specifies the variant to use for the add button. By default a normal button is used.
147157
* Use `inline-link` when using an attribute editor nested inside complex attribute editing

src/attribute-editor/internal.tsx

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { InternalBaseComponentProps } from '../internal/hooks/use-base-component
1515
import { usePrevious } from '../internal/hooks/use-previous';
1616
import { SomeRequired } from '../internal/types';
1717
import InternalLiveRegion from '../live-region/internal';
18+
import InternalSpaceBetween from '../space-between/internal';
1819
import { AdditionalInfo } from './additional-info';
1920
import { gridDefaults } from './grid-defaults';
2021
import { AttributeEditorForwardRefType, AttributeEditorProps } from './interfaces';
@@ -45,6 +46,8 @@ const InternalAttributeEditor = React.forwardRef(
4546
onAddButtonClick,
4647
onRemoveButtonClick,
4748
__internalRootRef,
49+
hideAddButton,
50+
additionalActions,
4851
...props
4952
}: InternalAttributeEditorProps<T>,
5053
ref: React.Ref<AttributeEditorProps.Ref>
@@ -153,24 +156,31 @@ const InternalAttributeEditor = React.forwardRef(
153156
))}
154157

155158
<div className={styles['add-row']}>
156-
<InternalButton
157-
className={styles['add-button']}
158-
disabled={disableAddButton}
159-
// Using aria-disabled="true" and tabindex="-1" instead of "disabled"
160-
// because focus can be dynamically moved to this button by calling
161-
// `focusAddButton()` on the ref.
162-
nativeButtonAttributes={disableAddButton ? { tabIndex: -1 } : {}}
163-
__skipNativeAttributesWarnings={true}
164-
__focusable={true}
165-
onClick={onAddButtonClick}
166-
formAction="none"
167-
ref={addButtonRef}
168-
ariaDescribedby={infoAriaDescribedBy}
169-
variant={addButtonVariant}
170-
iconName={addButtonVariant === 'inline-link' ? 'add-plus' : undefined}
171-
>
172-
{addButtonText}
173-
</InternalButton>
159+
{(!hideAddButton || additionalActions) && (
160+
<InternalSpaceBetween size="xs" direction="horizontal">
161+
{!hideAddButton && (
162+
<InternalButton
163+
className={styles['add-button']}
164+
disabled={disableAddButton}
165+
// Using aria-disabled="true" and tabindex="-1" instead of "disabled"
166+
// because focus can be dynamically moved to this button by calling
167+
// `focusAddButton()` on the ref.
168+
nativeButtonAttributes={disableAddButton ? { tabIndex: -1 } : {}}
169+
__skipNativeAttributesWarnings={true}
170+
__focusable={true}
171+
onClick={onAddButtonClick}
172+
formAction="none"
173+
ref={addButtonRef}
174+
ariaDescribedby={infoAriaDescribedBy}
175+
variant={addButtonVariant}
176+
iconName={addButtonVariant === 'inline-link' ? 'add-plus' : undefined}
177+
>
178+
{addButtonText}
179+
</InternalButton>
180+
)}
181+
{additionalActions}
182+
</InternalSpaceBetween>
183+
)}
174184
<InternalLiveRegion
175185
data-testid="removal-announcement"
176186
tagName="span"

0 commit comments

Comments
 (0)