diff --git a/pages/property-filter/custom-forms.scss b/pages/property-filter/custom-forms.scss index 50d26ed681..048d8e6c4e 100644 --- a/pages/property-filter/custom-forms.scss +++ b/pages/property-filter/custom-forms.scss @@ -10,8 +10,14 @@ grid-template-columns: auto; gap: awsui.$space-scaled-s; } + @media (min-width: 688px) { .date-time-form { grid-template-columns: repeat(2, minmax(100px, max-content)); } } + +.multiselect-form { + min-width: 200px; + max-width: 250px; +} diff --git a/pages/property-filter/custom-forms.tsx b/pages/property-filter/custom-forms.tsx index bcceb93c13..f4bb0fea24 100644 --- a/pages/property-filter/custom-forms.tsx +++ b/pages/property-filter/custom-forms.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react'; import { DatePicker, FormField, RadioGroup, TimeInput, TimeInputProps } from '~components'; import Calendar, { CalendarProps } from '~components/calendar'; import DateInput from '~components/date-input'; +import EmbeddedMultiselect from '~components/multiselect/embedded'; import InternalMultiselect from '~components/multiselect/internal'; import { ExtendedOperatorFormProps } from '~components/property-filter/interfaces'; @@ -220,11 +221,12 @@ function formatTimezoneOffset(isoDate: string, offsetInMinutes?: number) { const allOwners = [...new Set(allItems.map(({ owner }) => owner))]; -export function OwnerMultiSelectForm({ value, onChange }: ExtendedOperatorFormProps) { +export function OwnerMultiSelectForm({ value, onChange, filter }: ExtendedOperatorFormProps) { value = value && Array.isArray(value) ? value : []; - return ( - - ({ value: owner, label: owner }))} selectedOptions={value.map(owner => ({ value: owner, label: owner })) ?? []} onChange={event => @@ -234,14 +236,37 @@ export function OwnerMultiSelectForm({ value, onChange }: ExtendedOperatorFormPr .filter((value): value is string => typeof value !== 'undefined') ) } + filteringText={filter} statusType="finished" - filteringType="none" - expandToViewport={true} - keepOpen={true} - hideTokens={false} - inlineTokens={true} + filteringType="auto" + empty="No options available" + noMatch="No options matched" /> - + ); + } + + return ( +
+ + ({ value: owner, label: owner }))} + selectedOptions={value.map(owner => ({ value: owner, label: owner })) ?? []} + onChange={event => + onChange( + event.detail.selectedOptions + .map(({ value }) => value) + .filter((value): value is string => typeof value !== 'undefined') + ) + } + statusType="finished" + filteringType="none" + expandToViewport={true} + keepOpen={true} + hideTokens={false} + inlineTokens={true} + /> + +
); } diff --git a/src/multiselect/embedded.tsx b/src/multiselect/embedded.tsx index 42e2ff8ee6..9a912e381c 100644 --- a/src/multiselect/embedded.tsx +++ b/src/multiselect/embedded.tsx @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; -import DropdownFooter from '../internal/components/dropdown-footer/index.js'; -import ScreenreaderOnly from '../internal/components/screenreader-only/index.js'; +import { useFormFieldContext } from '../contexts/form-field'; +import DropdownFooter from '../internal/components/dropdown-footer'; +import ScreenreaderOnly from '../internal/components/screenreader-only'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import { SomeRequired } from '../internal/types'; import PlainList from '../select/parts/plain-list'; @@ -30,14 +31,15 @@ export type EmbeddedMultiselectProps = SomeRequired< | 'finishedText' | 'errorText' | 'recoveryText' + | 'empty' + | 'noMatch' >, - 'options' | 'selectedOptions' | 'filteringType' | 'statusType' | 'controlId' + 'options' | 'selectedOptions' | 'filteringType' | 'statusType' > & { filteringText?: string }; const EmbeddedMultiselect = React.forwardRef( ( { - controlId, options, filteringType, ariaLabel, @@ -49,8 +51,11 @@ const EmbeddedMultiselect = React.forwardRef( }: EmbeddedMultiselectProps, externalRef: React.Ref ) => { + const formFieldContext = useFormFieldContext(restProps); const ariaLabelId = useUniqueId('multiselect-ariaLabel-'); const footerId = useUniqueId('multiselect-footer-'); + const selfControlId = useUniqueId('multiselect-trigger-'); + const controlId = formFieldContext.controlId ?? selfControlId; const multiselectProps = useMultiselect({ options, diff --git a/src/property-filter/__tests__/property-filter-extended-operators.test.tsx b/src/property-filter/__tests__/property-filter-extended-operators.test.tsx index aac9183203..529ea586bd 100644 --- a/src/property-filter/__tests__/property-filter-extended-operators.test.tsx +++ b/src/property-filter/__tests__/property-filter-extended-operators.test.tsx @@ -6,6 +6,7 @@ import { act, render } from '@testing-library/react'; import { KeyCode } from '@cloudscape-design/test-utils-core/dist/utils.js'; +import Input from '../../../lib/components/input'; import PropertyFilter from '../../../lib/components/property-filter'; import { FilteringProperty, PropertyFilterProps, Ref } from '../../../lib/components/property-filter/interfaces.js'; import createWrapper, { PropertyFilterWrapper } from '../../../lib/components/test-utils/dom'; @@ -54,12 +55,26 @@ describe('extended operators', () => { ), }, + { + operator: '=', + form: ({ value, onChange }) => ( + onChange(detail.value)} /> + ), + }, ], propertyLabel: 'index', groupValuesLabel: 'index value', }; const extendedOperatorProps = { filteringProperties: [indexProperty] }; + test('property label is used to annotate custom form field', () => { + const { propertyFilterWrapper: wrapper } = renderComponent(extendedOperatorProps); + wrapper.setInputValue('index ='); + expect( + wrapper.findDropdown()!.findOpenDropdown()!.findInput()!.findNativeInput()!.getElement() + ).toHaveAccessibleName('index value'); + }); + test('property filter renders operator form instead of options list', () => { const { propertyFilterWrapper: wrapper } = renderComponent(extendedOperatorProps); wrapper.setInputValue('index >'); diff --git a/src/property-filter/property-editor.tsx b/src/property-filter/property-editor.tsx index f3b070b703..bf7943cb70 100644 --- a/src/property-filter/property-editor.tsx +++ b/src/property-filter/property-editor.tsx @@ -4,7 +4,8 @@ import React from 'react'; import InternalButton from '../button/internal'; -import InternalFormField from '../form-field/internal'; +import { FormFieldContext } from '../internal/context/form-field-context'; +import { useUniqueId } from '../internal/hooks/use-unique-id'; import { I18nStringsInternal } from './i18n-utils'; import { ComparisonOperator, ExtendedOperatorForm, InternalFilteringProperty, InternalToken } from './interfaces'; @@ -25,12 +26,16 @@ export function PropertyEditorContent({ onChange: (value: null | TokenValue) => void; operatorForm: ExtendedOperatorForm; }) { + const labelId = useUniqueId(); return (
+
+ {property.groupValuesLabel} +
- + {operatorForm({ value, onChange, operator, filter })} - +
); diff --git a/src/property-filter/styles.scss b/src/property-filter/styles.scss index c057c939bd..8f8d47f421 100644 --- a/src/property-filter/styles.scss +++ b/src/property-filter/styles.scss @@ -40,22 +40,21 @@ $operator-field-width: 120px; } .property-editor { - margin-block: awsui.$space-xxs; - margin-inline: awsui.$space-xxs; - padding-block: awsui.$space-m; - padding-inline: awsui.$space-m; overflow-y: auto; - &-field-property { - /* used in test-utils */ - } + &-header { + @include styles.default-text-style; + font-weight: bold; - &-field-operator { - margin-block-start: awsui.$space-scaled-l; + padding-block-start: awsui.$space-s; + padding-block-end: awsui.$space-xxs; + padding-inline: awsui.$space-s; } - &-field-value { - margin-block-start: awsui.$space-scaled-l; + &-form { + padding-block-start: awsui.$space-xxs; + padding-block-end: awsui.$space-s; + padding-inline: awsui.$space-s; } &-cancel {