Skip to content

Commit

Permalink
feat: Inline labels for select filters (#2355)
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Weber <[email protected]>
Co-authored-by: Gethin Webster <[email protected]>
  • Loading branch information
3 people committed Jul 25, 2024
1 parent 0bf4011 commit 8901807
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 33 deletions.
44 changes: 21 additions & 23 deletions pages/app-layout/with-table-collection-select-filter.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import AppLayout from '~components/app-layout';
import Box from '~components/box';
import Button from '~components/button';
import CollectionPreferences from '~components/collection-preferences';
import FormField from '~components/form-field';
import Header from '~components/header';
import Input from '~components/input';
import Link from '~components/link';
Expand Down Expand Up @@ -121,29 +120,28 @@ export default function () {
clearAriaLabel="clear"
/>
</div>
<div className="select-filter">
<FormField label={'Filter instance type'}>
<Select
data-testid="instance-type-filter"
options={instanceOptions}
selectedAriaLabel="Selected"
selectedOption={instanceOptions[0]}
onChange={() => {}}
expandToViewport={true}
/>
</FormField>
<div className={styles['select-filter']}>
<Select
data-testid="instance-type-filter"
inlineLabelText="Instance type"
options={instanceOptions}
selectedAriaLabel="Selected"
selectedOption={instanceOptions[0]}
onChange={() => {}}
expandToViewport={true}
/>
</div>
<div className="select-filter">
<FormField label={'Filter status'}>
<Select
data-testid="state-filter"
options={stateOptions}
selectedAriaLabel="Selected"
selectedOption={stateOptions[0]}
onChange={() => {}}
expandToViewport={true}
/>
</FormField>
<div className={styles['select-filter']}>
<Select
disabled={true}
data-testid="state-filter"
inlineLabelText="Filtrar secuencias de registros por nombre"
options={stateOptions}
selectedAriaLabel="Selected"
selectedOption={stateOptions[0]}
onChange={() => {}}
expandToViewport={true}
/>
</div>
</div>
}
Expand Down
5 changes: 2 additions & 3 deletions pages/app-layout/with-table-collection-select-filter.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@

.input-filter {
order: 0;
flex-grow: 6;
flex-grow: 1;
inline-size: auto;
max-inline-size: 728px;
}

.select-filter {
max-inline-size: 130px;
flex-grow: 2;
flex-grow: 1;
inline-size: auto;
}
9 changes: 8 additions & 1 deletion src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13272,7 +13272,7 @@ Use this if you don't have a visible label for this control.",
Object {
"description": "Adds \`aria-labelledby\` to the component. If you're using this component within a form field,
don't set this property because the form field component automatically sets it.
Use this property if the component isn't surrounded by a form field, or you want to override the value
Use this property if the component isn't using \`inlineLabelText\` and isn't surrounded by a form field, or you want to override the value
automatically set by the form field (for example, if you have two components within a single form field).

To use it correctly, define an ID for the element you want to use as label and set the property to that ID.
Expand Down Expand Up @@ -13409,6 +13409,13 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
"optional": true,
"type": "string",
},
Object {
"description": "Adds a small label inline with the input for saving vertical space in the UI.
For use with collection select filters only.",
"name": "inlineLabelText",
"optional": true,
"type": "string",
},
Object {
"description": "Overrides the invalidation state. Usually the invalid state
comes from the parent \`FormField\`component,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ Object {
],
"select": Array [
"awsui_disabled-reason-tooltip_dwuol",
"awsui_inline-label_dwuol",
"awsui_placeholder_dwuol",
"awsui_root_r2vco",
],
Expand Down
6 changes: 4 additions & 2 deletions src/select/__integ__/page-objects/select-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { strict as assert } from 'assert';

import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';

import { SelectWrapper } from '../../../../lib/components/test-utils/selectors';
import { MultiselectWrapper, SelectWrapper } from '../../../../lib/components/test-utils/selectors';

export default class SelectPageObject<Wrapper extends SelectWrapper = SelectWrapper> extends BasePageObject {
export default class SelectPageObject<
Wrapper extends SelectWrapper | MultiselectWrapper = SelectWrapper,
> extends BasePageObject {
constructor(
browser: ConstructorParameters<typeof BasePageObject>[0],
protected wrapper: Wrapper
Expand Down
20 changes: 20 additions & 0 deletions src/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,26 @@ describe.each([false, true])('expandToViewport=%s', expandToViewport => {
});
});

describe('Inline Label', () => {
test('should render', () => {
const testLabel = 'Test label';
const { wrapper } = renderSelect({ inlineLabelText: testLabel });
const labelElement = wrapper.findInlineLabel();
expect(labelElement).not.toBeNull();
expect(labelElement?.getElement()).toHaveTextContent(testLabel);
expect(labelElement?.getElement().tagName).toBe('LABEL');
});
test('associate label with trigger button', () => {
const testLabel = 'Test label';
const { wrapper } = renderSelect({ inlineLabelText: testLabel });

const labelForAttribute = wrapper.findInlineLabel()!.getElement()!.getAttribute('for');
const triggerId = wrapper.findTrigger().getElement()!.id;

expect(labelForAttribute).toBe(triggerId);
});
});

test('should render with focus when autoFocus=true', () => {
const { wrapper } = renderSelect({ autoFocus: true });
expect(wrapper.findTrigger().getElement()).toHaveFocus();
Expand Down
15 changes: 15 additions & 0 deletions src/select/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ export interface BaseSelectProps
}

export interface SelectProps extends BaseSelectProps {
/**
* Adds a small label inline with the input for saving vertical space in the UI.
* For use with collection select filters only.
*/
inlineLabelText?: string;
/**
* Adds `aria-labelledby` to the component. If you're using this component within a form field,
* don't set this property because the form field component automatically sets it.
*
* Use this property if the component isn't using `inlineLabelText` and isn't surrounded by a form field, or you want to override the value
* automatically set by the form field (for example, if you have two components within a single form field).
*
* To use it correctly, define an ID for the element you want to use as label and set the property to that ID.
*/
ariaLabelledby?: string;
/**
* Defines the variant of the trigger. You can use a simple label or the entire option (`label | option`)
*/
Expand Down
7 changes: 5 additions & 2 deletions src/select/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import DropdownFooter from '../internal/components/dropdown-footer';
import { useDropdownStatus } from '../internal/components/dropdown-status';
import { OptionGroup } from '../internal/components/option/interfaces.js';
import { prepareOptions } from '../internal/components/option/utils/prepare-options';
import ScreenreaderOnly from '../internal/components/screenreader-only/index.js';
import { useFormFieldContext } from '../internal/context/form-field-context';
import { fireNonCancelableEvent } from '../internal/events';
import checkControlled from '../internal/hooks/check-controlled';
Expand Down Expand Up @@ -47,6 +46,7 @@ const InternalSelect = React.forwardRef(
filteringAriaLabel,
filteringClearAriaLabel,
filteringResultsText,
inlineLabelText,
ariaRequired,
placeholder,
disabled,
Expand Down Expand Up @@ -174,6 +174,7 @@ const InternalSelect = React.forwardRef(
selectedOption={selectedOption}
isOpen={isOpen}
inFilteringToken={__inFilteringToken}
inlineLabelText={inlineLabelText}
{...formFieldContext}
controlId={controlId}
ariaLabelledby={joinStrings(formFieldContext.ariaLabelledby, selectAriaLabelId)}
Expand Down Expand Up @@ -275,7 +276,9 @@ const InternalSelect = React.forwardRef(
highlightType={highlightType}
/>
</Dropdown>
<ScreenreaderOnly id={selectAriaLabelId}>{ariaLabel}</ScreenreaderOnly>
<div hidden={true} id={selectAriaLabelId}>
{ariaLabel || inlineLabelText}
</div>
</div>
);
}
Expand Down
41 changes: 41 additions & 0 deletions src/select/parts/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
}

$checkbox-size: awsui.$size-control;
$inlineLabel-border-radius: 2px;

.item {
display: flex;
Expand Down Expand Up @@ -122,6 +123,46 @@ $checkbox-size: awsui.$size-control;
}
}

.inline-label-trigger-wrapper {
margin-block-start: -7px;
}

.inline-label-wrapper {
margin-block-start: calc(awsui.$space-scaled-xs * -1);
}

.inline-label {
// Stepped gradient background for inline label overlap between input and background.
background-image: linear-gradient(
to bottom,
transparent calc(100% - (awsui.$border-field-width + awsui.$border-control-focus-ring-shadow-spread + 5px)),
awsui.$color-background-input-default 1px
);
background-position: bottom;
box-sizing: border-box;
display: inline-block;
color: awsui.$color-text-form-label;
font-weight: awsui.$font-display-label-weight;
font-size: awsui.$font-size-body-s;
line-height: 14px;
letter-spacing: awsui.$letter-spacing-body-s;
position: relative;
inset-inline-start: calc(styles.$control-border-width + awsui.$space-field-horizontal - awsui.$space-scaled-xxs);
margin-block-start: awsui.$space-scaled-xs;
padding-block-end: 2px;
padding-inline: awsui.$space-scaled-xxs;
max-inline-size: calc(100% - (2 * awsui.$space-field-horizontal));
z-index: 1;

&.inline-label-disabled {
background: awsui.$color-background-container-header;
border-start-start-radius: $inlineLabel-border-radius;
border-start-end-radius: $inlineLabel-border-radius;
border-end-start-radius: $inlineLabel-border-radius;
border-end-end-radius: $inlineLabel-border-radius;
}
}

.disabled-reason-tooltip {
/* used in test-utils or tests */
}
22 changes: 20 additions & 2 deletions src/select/parts/trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TriggerProps extends FormFieldValidationControlProps {
readOnly?: boolean;
triggerProps: SelectTriggerProps;
selectedOption: OptionDefinition | null;
inlineLabelText?: string;
isOpen?: boolean;
triggerVariant?: SelectProps.TriggerVariant | MultiselectProps.TriggerVariant;
inFilteringToken?: boolean;
Expand All @@ -36,6 +37,7 @@ const Trigger = React.forwardRef(
ariaDescribedby,
controlId,
invalid,
inlineLabelText,
warning,
triggerProps,
selectedOption,
Expand Down Expand Up @@ -105,8 +107,7 @@ const Trigger = React.forwardRef(
}

const mergedRef = useMergeRefs(triggerProps.ref, ref);

return (
const triggerButton = (
<ButtonTrigger
{...triggerProps}
id={id}
Expand All @@ -124,6 +125,23 @@ const Trigger = React.forwardRef(
{triggerContent}
</ButtonTrigger>
);
return (
<>
{inlineLabelText ? (
<div className={styles['inline-label-wrapper']}>
<label
htmlFor={controlId}
className={clsx(styles['inline-label'], disabled && styles['inline-label-disabled'])}
>
{inlineLabelText}
</label>
<div className={styles['inline-label-trigger-wrapper']}>{triggerButton}</div>
</div>
) : (
<>{triggerButton}</>
)}
</>
);
}
);

Expand Down
4 changes: 4 additions & 0 deletions src/test-utils/dom/select/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export default class SelectWrapper extends DropdownHostComponentWrapper {
return this.findDropdown(options).findComponent(`.${inputStyles['input-container']}`, InputWrapper);
}

findInlineLabel(): ElementWrapper | null {
return this.findByClassName(selectPartsStyles['inline-label']);
}

findPlaceholder(): ElementWrapper | null {
return this.findByClassName(selectPartsStyles.placeholder);
}
Expand Down

0 comments on commit 8901807

Please sign in to comment.