Skip to content

Commit 9524306

Browse files
authored
feat: Add Inline label text to multiselect (#3737)
1 parent 77f4711 commit 9524306

File tree

8 files changed

+114
-2
lines changed

8 files changed

+114
-2
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React from 'react';
4+
5+
import Multiselect, { MultiselectProps } from '~components/multiselect';
6+
7+
import createPermutations from '../utils/permutations';
8+
import PermutationsView from '../utils/permutations-view';
9+
import ScreenshotArea from '../utils/screenshot-area';
10+
import { deselectAriaLabel, i18nStrings } from './constants';
11+
12+
const options: MultiselectProps.Options = [
13+
{ value: 'first', label: 'Simple' },
14+
{ value: 'second', label: 'Second option' },
15+
];
16+
17+
const permutations = createPermutations<MultiselectProps>([
18+
{
19+
placeholder: ['Select an item'],
20+
disabled: [false, true],
21+
options: [options],
22+
selectedOptions: [[], [options[0], options[1]]],
23+
deselectAriaLabel: [deselectAriaLabel],
24+
i18nStrings: [i18nStrings],
25+
inlineLabelText: [
26+
'Inline label',
27+
'Very long inline label that should wrap into multiple lines on narrow enough viewports',
28+
],
29+
inlineTokens: [undefined, true],
30+
},
31+
]);
32+
33+
export default function MultiselectInlineLabelTextPermutations() {
34+
return (
35+
<>
36+
<h1>Multiselect inlineLabelText permutations</h1>
37+
<ScreenshotArea>
38+
<PermutationsView permutations={permutations} render={permutation => <Multiselect {...permutation} />} />
39+
</ScreenshotArea>
40+
</>
41+
);
42+
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16468,6 +16468,12 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
1646816468
"optional": true,
1646916469
"type": "string",
1647016470
},
16471+
{
16472+
"description": "Specifies an inline label that appears next to the multiselect trigger.",
16473+
"name": "inlineLabelText",
16474+
"optional": true,
16475+
"type": "string",
16476+
},
1647116477
{
1647216478
"description": "Shows tokens inside the trigger instead of below it.",
1647316479
"name": "inlineTokens",
@@ -74560,6 +74566,14 @@ Component's wrapper class",
7456074566
"type": "union",
7456174567
},
7456274568
},
74569+
{
74570+
"name": "findInlineLabel",
74571+
"parameters": [],
74572+
"returnType": {
74573+
"name": "ElementWrapper | null",
74574+
"type": "union",
74575+
},
74576+
},
7456374577
{
7456474578
"name": "findPlaceholder",
7456574579
"parameters": [],
@@ -132505,6 +132519,15 @@ Component's wrapper class",
132505132519
"typeArguments": [],
132506132520
},
132507132521
},
132522+
{
132523+
"name": "findInlineLabel",
132524+
"parameters": [],
132525+
"returnType": {
132526+
"name": "ElementWrapper",
132527+
"type": "reference",
132528+
"typeArguments": [],
132529+
},
132530+
},
132508132531
{
132509132532
"name": "findPlaceholder",
132510132533
"parameters": [],

src/multiselect/__tests__/multiselect.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,30 @@ test('does not render tokens when no tokens are present', () => {
155155
expect(wrapper.findTokens()).toHaveLength(0);
156156
});
157157

158+
test('renders inline label text when provided', () => {
159+
const { wrapper } = renderMultiselect(
160+
<Multiselect selectedOptions={[]} options={defaultOptions} inlineLabelText="Select items:" />
161+
);
162+
const inlineLabel = wrapper.findInlineLabel();
163+
expect(inlineLabel).not.toBeNull();
164+
expect(inlineLabel!.getElement()).toHaveTextContent('Select items:');
165+
});
166+
167+
test('does not render inline label when inlineLabelText is not provided', () => {
168+
const { wrapper } = renderMultiselect(<Multiselect selectedOptions={[]} options={defaultOptions} />);
169+
const inlineLabel = wrapper.findInlineLabel();
170+
expect(inlineLabel).toBeNull();
171+
});
172+
173+
test('associate label with trigger button', () => {
174+
const { wrapper } = renderMultiselect(
175+
<Multiselect selectedOptions={[]} options={defaultOptions} inlineLabelText="Select items:" />
176+
);
177+
const labelForAttribute = wrapper.findInlineLabel()!.getElement()!.getAttribute('for');
178+
const triggerId = wrapper.findTrigger().getElement()!.id;
179+
expect(labelForAttribute).toBe(triggerId);
180+
});
181+
158182
test('allows deselecting an option without object equality', () => {
159183
const onChange = jest.fn();
160184
const { wrapper } = renderMultiselect(

src/multiselect/interfaces.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export interface MultiselectProps extends BaseSelectProps {
1010
* Provide an empty array to clear the selection.
1111
*/
1212
selectedOptions: ReadonlyArray<MultiselectProps.Option>;
13+
/**
14+
* Specifies an inline label that appears next to the multiselect trigger.
15+
*/
16+
inlineLabelText?: string;
1317
/**
1418
* Determines whether the dropdown list stays open after the user selects an item.
1519
*/

src/multiselect/internal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const InternalMultiselect = React.forwardRef(
4646
disabled,
4747
readOnly,
4848
ariaLabel,
49+
inlineLabelText,
4950
selectedOptions,
5051
deselectAriaLabel,
5152
tokenLimit,
@@ -112,6 +113,7 @@ const InternalMultiselect = React.forwardRef(
112113
selectedOptions={selectedOptions}
113114
triggerVariant={inlineTokens ? 'tokens' : 'placeholder'}
114115
isOpen={multiselectProps.isOpen}
116+
inlineLabelText={inlineLabelText}
115117
{...formFieldContext}
116118
controlId={controlId}
117119
ariaLabelledby={joinStrings(formFieldContext.ariaLabelledby, ariaLabelId)}
@@ -207,7 +209,7 @@ const InternalMultiselect = React.forwardRef(
207209
/>
208210
)}
209211

210-
<ScreenreaderOnly id={ariaLabelId}>{ariaLabel}</ScreenreaderOnly>
212+
<ScreenreaderOnly id={ariaLabelId}>{ariaLabel || inlineLabelText}</ScreenreaderOnly>
211213
</div>
212214
);
213215
}

src/select/parts/styles.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ $inlineLabel-border-radius: 2px;
153153
z-index: 1;
154154
}
155155

156+
// Specific Styling for Inline Label Text to not overlap with Inline Tokens
157+
.inline-label-inline-tokens {
158+
padding-block-end: 0;
159+
transform: translateY(-1.5px);
160+
}
161+
156162
.disabled-reason-tooltip {
157163
/* used in test-utils or tests */
158164
}

src/select/parts/trigger.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,13 @@ const Trigger = React.forwardRef(
129129
<>
130130
{inlineLabelText ? (
131131
<div className={styles['inline-label-wrapper']}>
132-
<label htmlFor={controlId} className={styles['inline-label']}>
132+
<label
133+
htmlFor={controlId}
134+
className={clsx(
135+
styles['inline-label'],
136+
triggerVariant === 'tokens' && styles['inline-label-inline-tokens']
137+
)}
138+
>
133139
{inlineLabelText}
134140
</label>
135141
<div className={styles['inline-label-trigger-wrapper']}>{triggerButton}</div>

src/test-utils/dom/multiselect/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import buttonTriggerStyles from '../../../internal/components/button-trigger/sty
1212
import dropdownStatusStyles from '../../../internal/components/dropdown-status/styles.selectors.js';
1313
import footerStyles from '../../../internal/components/dropdown-status/styles.selectors.js';
1414
import multiselectStyles from '../../../multiselect/styles.selectors.js';
15+
import selectPartsStyles from '../../../select/parts/styles.selectors.js';
1516
import tokenGroupStyles from '../../../token-group/styles.selectors.js';
1617

1718
export default class MultiselectWrapper extends DropdownHostComponentWrapper {
@@ -38,6 +39,10 @@ export default class MultiselectWrapper extends DropdownHostComponentWrapper {
3839
return this.findByClassName(buttonTriggerStyles.placeholder);
3940
}
4041

42+
findInlineLabel(): ElementWrapper | null {
43+
return this.findByClassName(selectPartsStyles['inline-label']);
44+
}
45+
4146
/**
4247
* @param options
4348
* * expandToViewport (boolean) - Use this when the component under test is rendered with an `expandToViewport` flag.

0 commit comments

Comments
 (0)