From b69a1a3ec0bf984ee51198fbfbb5a956d9f73768 Mon Sep 17 00:00:00 2001 From: Vahab Valiyev Date: Wed, 28 Oct 2026 11:42:55 +0100 Subject: [PATCH] chore: Adds internal style api v2 to text filter, date input and time input --- pages/date-input/style-api-v2.page.tsx | 78 ++++++ pages/style-api/input-field.scss | 99 +++++++ pages/style-api/input-field.tsx | 31 +++ pages/text-filter/style-api-v2.page.tsx | 94 +++++++ pages/text-filter/style-api-v2.scss | 40 +++ pages/time-input/style-api-v2.page.tsx | 78 ++++++ .../__snapshots__/style-docs.test.ts.snap | 250 +++++++++++++++++- src/date-input/__tests__/style-api.test.tsx | 45 ++++ src/date-input/internal.tsx | 9 + src/date-input/styles.scss | 4 + src/text-filter/__tests__/style-api.test.tsx | 59 +++++ src/text-filter/internal.tsx | 19 +- src/text-filter/search-results.tsx | 6 +- src/text-filter/styles.scss | 11 + src/time-input/__tests__/style-api.test.tsx | 45 ++++ src/time-input/internal.tsx | 12 +- src/time-input/styles.scss | 4 + 17 files changed, 868 insertions(+), 16 deletions(-) create mode 100644 pages/date-input/style-api-v2.page.tsx create mode 100644 pages/style-api/input-field.scss create mode 100644 pages/style-api/input-field.tsx create mode 100644 pages/text-filter/style-api-v2.page.tsx create mode 100644 pages/text-filter/style-api-v2.scss create mode 100644 pages/time-input/style-api-v2.page.tsx create mode 100644 src/date-input/__tests__/style-api.test.tsx create mode 100644 src/text-filter/__tests__/style-api.test.tsx create mode 100644 src/time-input/__tests__/style-api.test.tsx diff --git a/pages/date-input/style-api-v2.page.tsx b/pages/date-input/style-api-v2.page.tsx new file mode 100644 index 00000000000..aef0cfdd955 --- /dev/null +++ b/pages/date-input/style-api-v2.page.tsx @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { DateInput, DateInputProps, KeyValuePairs } from '~components'; + +import { SimplePage } from '../app/templates'; +import { getInputFieldClassNames } from '../style-api/input-field'; + +// The `input` slot forwards to the whole Input style API; the shared helper maps the owned states to classes. +function StyledDateInput({ value: initialValue, invalid, warning, disabled, readOnly, ...rest }: DateInputProps) { + const [value, setValue] = useState(initialValue); + return ( + setValue(detail.value)} + invalid={invalid} + warning={warning} + disabled={disabled} + readOnly={readOnly} + {...{ styleClassNames: { input: getInputFieldClassNames({ invalid, warning, disabled, readOnly }) } }} + /> + ); +} + +export default function () { + return ( + + , + }, + { + type: 'pair', + label: 'Placeholder', + value: , + }, + { + type: 'pair', + label: 'Invalid', + value: , + }, + { + type: 'pair', + label: 'Warning', + value: , + }, + { + type: 'pair', + label: 'Disabled', + value: , + }, + { + type: 'pair', + label: 'Disabled with placeholder', + value: , + }, + { + type: 'pair', + label: 'Read-only', + value: , + }, + { + type: 'pair', + label: 'Long localized format', + value: , + }, + ]} + /> + + ); +} diff --git a/pages/style-api/input-field.scss b/pages/style-api/input-field.scss new file mode 100644 index 00000000000..5f1be91518e --- /dev/null +++ b/pages/style-api/input-field.scss @@ -0,0 +1,99 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +// Shared demo styling for components whose `input` slot forwards to Input. The field box is styled with +// Input.root, one class per owned state (invalid, warning, disabled, read-only), `:hover` and `:focus-within` +// for the interaction states; the native input with Input.input. +@use '~components/styling/input'; +@use './palette' as palette; + +@mixin field-box { + @include input.root( + $background: palette.$surface, + $border-width: 2px, + $border-radius: 12px, + $box-shadow: 0 2px 4px palette.$subtle + ); +} + +.input-root { + @include field-box; + @include input.root($border-color: palette.$border); + + &:hover { + @include input.root($background: palette.$surface-alt, $border-color: palette.$border-accent); + } + + &:focus-within { + @include input.root($border-color: palette.$accent, $box-shadow: 0 0 0 3px palette.$accent-active); + } +} + +.input-root-invalid { + @include field-box; + @include input.root($background: palette.$error-surface, $border-color: palette.$error); + + &:focus-within { + @include input.root($box-shadow: 0 0 0 3px palette.$error-hover); + } +} + +.input-root-warning { + @include field-box; + @include input.root($background: palette.$warning-surface, $border-color: palette.$warning); + + &:focus-within { + @include input.root($box-shadow: 0 0 0 3px palette.$warning); + } +} + +.input-root-disabled { + @include field-box; + @include input.root( + $background: palette.$control-bg-disabled, + $border-color: palette.$border-disabled, + $box-shadow: none + ); +} + +.input-root-readonly { + @include field-box; + @include input.root($background: palette.$surface-alt, $border-color: palette.$border); + + &:focus-within { + @include input.root($border-color: palette.$accent, $box-shadow: 0 0 0 3px palette.$accent-active); + } +} + +.input-input { + @include input.input( + $color: palette.$text, + $padding-block: 10px, + $padding-inline: 16px, + $font-family: monospace, + $font-size: 15px, + $font-weight: 600, + $placeholder-color: palette.$accent, + $placeholder-font-size: 13px, + $placeholder-font-style: normal, + $placeholder-font-weight: 400 + ); + + &:focus { + @include input.input($color: palette.$accent); + } + + &:placeholder-shown { + @include input.input($padding-inline: 24px); + } +} + +.input-input-disabled { + @include input.input($color: palette.$text-disabled); +} + +.input-input-readonly { + @include input.input($color: palette.$text); +} diff --git a/pages/style-api/input-field.tsx b/pages/style-api/input-field.tsx new file mode 100644 index 00000000000..31ddb79df37 --- /dev/null +++ b/pages/style-api/input-field.tsx @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import clsx from 'clsx'; + +import styles from './input-field.scss'; + +export interface InputFieldState { + invalid?: boolean; + warning?: boolean; + disabled?: boolean; + readOnly?: boolean; +} + +// Class names for a forwarded Input in one of its owned states: one class for the field box (Input.root), one +// for the native input (Input.input). The interaction states are handled by selectors in input-field.scss. +export function getInputFieldClassNames({ invalid, warning, disabled, readOnly }: InputFieldState) { + return { + root: clsx({ + [styles['input-root-invalid']]: invalid, + [styles['input-root-warning']]: warning && !invalid, + [styles['input-root-disabled']]: disabled && !invalid && !warning, + [styles['input-root-readonly']]: readOnly && !disabled && !invalid && !warning, + [styles['input-root']]: !invalid && !warning && !disabled && !readOnly, + }), + input: clsx( + styles['input-input'], + disabled && styles['input-input-disabled'], + readOnly && !disabled && styles['input-input-readonly'] + ), + }; +} diff --git a/pages/text-filter/style-api-v2.page.tsx b/pages/text-filter/style-api-v2.page.tsx new file mode 100644 index 00000000000..f93c5ba990c --- /dev/null +++ b/pages/text-filter/style-api-v2.page.tsx @@ -0,0 +1,94 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; +import clsx from 'clsx'; + +import { FormField, KeyValuePairs, TextFilter, TextFilterProps } from '~components'; + +import { SimplePage } from '../app/templates'; +import { getInputFieldClassNames } from '../style-api/input-field'; + +import styles from './style-api-v2.scss'; + +// The `input` slot forwards to the whole Input style API; the shared helper maps the owned states (disabled, and +// invalid or warning from the surrounding form field) to classes. +function StyledTextFilter({ + filteringText: initialFilteringText, + disabled, + invalid, + warning, + ...rest +}: Omit & { invalid?: boolean; warning?: boolean }) { + const [filteringText, setFilteringText] = useState(initialFilteringText); + const fieldClassNames = getInputFieldClassNames({ invalid, warning, disabled }); + const textFilter = ( + setFilteringText(detail.filteringText)} + disabled={disabled} + {...{ + styleClassNames: { + input: { + ...fieldClassNames, + input: clsx(fieldClassNames.input, styles['input-input-search']), + searchIcon: clsx(styles['input-search-icon'], disabled && styles['input-search-icon-disabled']), + clearButton: styles['input-clear-button'], + }, + countText: styles['count-text'], + }, + }} + /> + ); + if (invalid || warning) { + return ( + + {textFilter} + + ); + } + return textFilter; +} + +export default function () { + return ( + + , + }, + { + type: 'pair', + label: 'Placeholder', + value: , + }, + { + type: 'pair', + label: 'Disabled with count text', + value: , + }, + { + type: 'pair', + label: 'Disabled with placeholder', + value: , + }, + { + type: 'pair', + label: 'Invalid', + value: , + }, + { + type: 'pair', + label: 'Warning', + value: , + }, + ]} + /> + + ); +} diff --git a/pages/text-filter/style-api-v2.scss b/pages/text-filter/style-api-v2.scss new file mode 100644 index 00000000000..5657ffb5599 --- /dev/null +++ b/pages/text-filter/style-api-v2.scss @@ -0,0 +1,40 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~components/styling/input'; +@use '~components/styling/button'; +@use '~components/styling/text-filter'; +@use '../style-api/palette' as palette; + +// The filter is a search input: the inline padding replaces the room the component reserves for the search icon +// and the clear button, so it is set with that room included, in every state. Added to the shared input class. +.input-input-search, +.input-input-search:placeholder-shown { + @include input.input($padding-inline: 40px); +} + +// Search icon (Input.searchIcon): the disabled state is a class. +.input-search-icon { + @include input.searchIcon($color: palette.$accent); +} + +.input-search-icon-disabled { + @include input.searchIcon($color: palette.$text-disabled); +} + +// Clear button (Input.clearButton) forwards to Button.root. +.input-clear-button { + @include button.root($color: palette.$accent, $focus-ring-color: palette.$accent); + + &:hover, + &:focus { + @include button.root($color: palette.$accent-hover); + } +} + +// The count text is the one slot Text filter owns. +.count-text { + @include text-filter.countText($color: palette.$accent, $font-family: monospace, $font-size: 13px, $font-weight: 700); +} diff --git a/pages/time-input/style-api-v2.page.tsx b/pages/time-input/style-api-v2.page.tsx new file mode 100644 index 00000000000..bbf0d9b2a78 --- /dev/null +++ b/pages/time-input/style-api-v2.page.tsx @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { KeyValuePairs, TimeInput, TimeInputProps } from '~components'; + +import { SimplePage } from '../app/templates'; +import { getInputFieldClassNames } from '../style-api/input-field'; + +// The `input` slot forwards to the whole Input style API; the shared helper maps the owned states to classes. +function StyledTimeInput({ value: initialValue, invalid, warning, disabled, readOnly, ...rest }: TimeInputProps) { + const [value, setValue] = useState(initialValue); + return ( + setValue(detail.value)} + invalid={invalid} + warning={warning} + disabled={disabled} + readOnly={readOnly} + {...{ styleClassNames: { input: getInputFieldClassNames({ invalid, warning, disabled, readOnly }) } }} + /> + ); +} + +export default function () { + return ( + + , + }, + { + type: 'pair', + label: 'Placeholder', + value: , + }, + { + type: 'pair', + label: 'Invalid', + value: , + }, + { + type: 'pair', + label: 'Warning', + value: , + }, + { + type: 'pair', + label: 'Disabled', + value: , + }, + { + type: 'pair', + label: 'Disabled with placeholder', + value: , + }, + { + type: 'pair', + label: 'Read-only', + value: , + }, + { + type: 'pair', + label: '12-hour format', + value: , + }, + ]} + /> + + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap index f4b2750ac69..3227d8dedc5 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap @@ -8,9 +8,23 @@ exports[`Style API docs definition for alert matches the snapshot: alert 1`] = ` "properties": [ "color", "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "padding", + "padding-block", + "padding-inline", ], "propertyDescriptions": {}, "tokenDescriptions": {}, @@ -48,9 +62,23 @@ exports[`Style API docs definition for badge matches the snapshot: badge 1`] = ` "properties": [ "color", "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "padding", + "padding-block", + "padding-inline", "font-family", "font-size", "font-weight", @@ -71,9 +99,23 @@ exports[`Style API docs definition for button matches the snapshot: button 1`] = "properties": [ "color", "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "padding", + "padding-block", + "padding-inline", "box-shadow", ], "propertyDescriptions": {}, @@ -88,6 +130,65 @@ exports[`Style API docs definition for button matches the snapshot: button 1`] = } `; +exports[`Style API docs definition for button-dropdown matches the snapshot: button-dropdown 1`] = ` +{ + "slots": [ + { + "forwardsTo": { + "component": "button", + "slot": "root", + }, + "name": "trigger", + }, + { + "forwardsTo": { + "component": "button", + "slot": "root", + }, + "name": "mainAction", + }, + { + "forwardsTo": { + "component": "dropdown", + "slot": "container", + }, + "name": "dropdown", + }, + ], +} +`; + +exports[`Style API docs definition for button-group matches the snapshot: button-group 1`] = ` +{ + "slots": [ + { + "name": "divider", + "properties": [], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [ + "line-color", + "line-width", + ], + }, + { + "forwardsTo": { + "component": "button", + "slot": "root", + }, + "name": "item", + }, + { + "forwardsTo": { + "component": "dropdown", + "slot": "container", + }, + "name": "dropdown", + }, + ], +} +`; + exports[`Style API docs definition for checkbox matches the snapshot: checkbox 1`] = ` { "slots": [ @@ -116,7 +217,15 @@ exports[`Style API docs definition for container matches the snapshot: container "name": "root", "properties": [ "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "box-shadow", ], "propertyDescriptions": {}, @@ -132,7 +241,15 @@ exports[`Style API docs definition for container matches the snapshot: container "name": "header", "properties": [ "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", ], "propertyDescriptions": {}, "tokenDescriptions": {}, @@ -143,7 +260,15 @@ exports[`Style API docs definition for container matches the snapshot: container "name": "footer", "properties": [ "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", ], "propertyDescriptions": {}, "tokenDescriptions": {}, @@ -153,14 +278,48 @@ exports[`Style API docs definition for container matches the snapshot: container } `; +exports[`Style API docs definition for date-input matches the snapshot: date-input 1`] = ` +{ + "slots": [ + { + "forwardsTo": { + "component": "input", + }, + "name": "input", + }, + ], +} +`; + +exports[`Style API docs definition for divider matches the snapshot: divider 1`] = ` +{ + "slots": [ + { + "name": "root", + "properties": [], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [ + "line-color", + "line-width", + ], + }, + ], +} +`; + exports[`Style API docs definition for dropdown matches the snapshot: dropdown 1`] = ` { "slots": [ { - "name": "dropdown", + "name": "container", "properties": [ "background", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "box-shadow", ], "propertyDescriptions": {}, @@ -184,8 +343,20 @@ exports[`Style API docs definition for input matches the snapshot: input 1`] = ` "name": "root", "properties": [ "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "box-shadow", ], "propertyDescriptions": { @@ -199,6 +370,8 @@ exports[`Style API docs definition for input matches the snapshot: input 1`] = ` "properties": [ "color", "padding", + "padding-block", + "padding-inline", "font-family", "font-size", "font-weight", @@ -292,6 +465,32 @@ exports[`Style API docs definition for radio-button matches the snapshot: radio- } `; +exports[`Style API docs definition for text-filter matches the snapshot: text-filter 1`] = ` +{ + "slots": [ + { + "forwardsTo": { + "component": "input", + }, + "name": "input", + }, + { + "description": "The results count rendered after the input. It is shown while \`filteringText\` and \`countText\` are set and the filter is not disabled.", + "name": "countText", + "properties": [ + "color", + "font-family", + "font-size", + "font-weight", + ], + "propertyDescriptions": {}, + "tokenDescriptions": {}, + "tokens": [], + }, + ], +} +`; + exports[`Style API docs definition for textarea matches the snapshot: textarea 1`] = ` { "slots": [ @@ -300,10 +499,24 @@ exports[`Style API docs definition for textarea matches the snapshot: textarea 1 "properties": [ "color", "background", - "border", + "border-color", + "border-width", + "border-style", + "border-block-color", + "border-block-width", + "border-block-style", + "border-inline-color", + "border-inline-width", + "border-inline-style", "border-radius", + "border-start-start-radius", + "border-start-end-radius", + "border-end-start-radius", + "border-end-end-radius", "box-shadow", "padding", + "padding-block", + "padding-inline", "font-family", "font-size", "font-weight", @@ -323,6 +536,19 @@ exports[`Style API docs definition for textarea matches the snapshot: textarea 1 } `; +exports[`Style API docs definition for time-input matches the snapshot: time-input 1`] = ` +{ + "slots": [ + { + "forwardsTo": { + "component": "input", + }, + "name": "input", + }, + ], +} +`; + exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] = ` { "slots": [ @@ -348,13 +574,19 @@ exports[`Style API docs list of components with a Style API doc matches the snap "alert", "badge", "button", + "button-dropdown", + "button-group", "checkbox", "container", + "date-input", + "divider", "dropdown", "input", "link", "radio-button", + "text-filter", "textarea", + "time-input", "toggle", ] `; diff --git a/src/date-input/__tests__/style-api.test.tsx b/src/date-input/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..3923afe9335 --- /dev/null +++ b/src/date-input/__tests__/style-api.test.tsx @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import DateInput, { DateInputProps } from '../../../lib/components/date-input'; +import createWrapper from '../../../lib/components/test-utils/dom'; + +function renderDateInput( + props: Omit & { + value?: string; + styleClassNames?: Record>; + } = {} +) { + const { container } = render( {}} ariaLabel="Date" {...props} />); + return { container, wrapper: createWrapper(container).findDateInput()! }; +} + +describe('Date input Style API v2', () => { + test('forwards the input classes to the Input slots and ignores unknown keys', () => { + const { container, wrapper } = renderDateInput({ + value: '2024-03-15', + styleClassNames: { input: { root: 'a', input: 'b', unknown: 'c' }, unknown: 'd' }, + }); + const nativeInput = wrapper.findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + expect(container.querySelector('.c')).toBeNull(); + expect(container.querySelector('.d')).toBeNull(); + }); + + test('forwards the input classes in every state', () => { + for (const state of [{}, { invalid: true }, { warning: true }, { disabled: true }, { readOnly: true }]) { + const { wrapper } = renderDateInput({ ...state, styleClassNames: { input: { root: 'a', input: 'b' } } }); + const nativeInput = wrapper.findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + } + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderDateInput({ styleClassNames: { input: { root: 'a', input: 'b' } } }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/date-input/internal.tsx b/src/date-input/internal.tsx index 23370b4982e..81eac88c9d1 100644 --- a/src/date-input/internal.tsx +++ b/src/date-input/internal.tsx @@ -6,6 +6,7 @@ import clsx from 'clsx'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import { StyleClassNames as InputStyleClassNames } from '../input/internal'; import MaskedInput from '../internal/components/masked-input'; import { fireNonCancelableEvent } from '../internal/events'; import useForwardFocus from '../internal/hooks/forward-focus'; @@ -23,8 +24,14 @@ import styles from './styles.css.js'; type InternalDateInputProps = DateInputProps & InternalBaseComponentProps & { __injectAnalyticsComponentMetadata?: boolean; + styleClassNames?: StyleClassNames; }; +// Style API v2 +interface StyleClassNames { + input?: InputStyleClassNames; +} + const InternalDateInput = React.forwardRef( ( { @@ -36,6 +43,7 @@ const InternalDateInput = React.forwardRef( locale, format = 'slashed', inputFormat = 'slashed', + styleClassNames, __internalRootRef, __injectAnalyticsComponentMetadata = false, ...props @@ -105,6 +113,7 @@ const InternalDateInput = React.forwardRef( disableBrowserAutocorrect={true} showUnmaskedValue={usesLongLocalizedValue} autoComplete={false} + styleClassNames={styleClassNames?.input} __internalRootRef={__internalRootRef} {...(__injectAnalyticsComponentMetadata ? getAnalyticsMetadataAttribute({ component: componentAnalyticsMetadata }) diff --git a/src/date-input/styles.scss b/src/date-input/styles.scss index 5a54f6dcc39..6d57ab777ef 100644 --- a/src/date-input/styles.scss +++ b/src/date-input/styles.scss @@ -3,6 +3,10 @@ SPDX-License-Identifier: Apache-2.0 */ +@use '../internal/styles/style-api' as style-api; + +@include style-api.docs-forward('input', 'input'); + .root { /* used in test-utils */ } diff --git a/src/text-filter/__tests__/style-api.test.tsx b/src/text-filter/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..c7571e01ed2 --- /dev/null +++ b/src/text-filter/__tests__/style-api.test.tsx @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import createWrapper from '../../../lib/components/test-utils/dom'; +import TextFilter, { TextFilterProps } from '../../../lib/components/text-filter'; + +function renderTextFilter( + props: Omit & { + filteringText?: string; + styleClassNames?: Record>; + } = {} +) { + const { container } = render( + {}} filteringAriaLabel="Filter" {...props} /> + ); + return { container, wrapper: createWrapper(container).findTextFilter()! }; +} + +describe('Text filter Style API v2', () => { + test('forwards the input classes to the Input slots and applies the countText class', () => { + const { container, wrapper } = renderTextFilter({ + filteringText: 'query', + countText: '18 matches', + styleClassNames: { + input: { root: 'a', input: 'b', searchIcon: 'c', clearButton: 'd', unknown: 'e' }, + countText: 'f', + unknown: 'g', + }, + }); + const nativeInput = wrapper.findInput().findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + expect(container.querySelector('.c')).toBe(nativeInput.parentElement!.previousElementSibling); + expect(wrapper.findInput().findClearButton()!.getElement()).toHaveClass('d'); + expect(container.querySelector('.e')).toBeNull(); + expect(wrapper.findResultsCount().getElement()).toHaveClass('f'); + expect(container.querySelector('.g')).toBeNull(); + }); + + test('applies the input classes when disabled and renders no countText element', () => { + const { container, wrapper } = renderTextFilter({ + filteringText: 'query', + countText: '18 matches', + disabled: true, + styleClassNames: { input: { root: 'a', input: 'b' }, countText: 'c' }, + }); + const nativeInput = wrapper.findInput().findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + expect(container.querySelector('.c')).toBeNull(); + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderTextFilter({ styleClassNames: { input: { root: 'a' }, countText: 'b' } }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/text-filter/internal.tsx b/src/text-filter/internal.tsx index 90f1abd5135..2919a4ebf31 100644 --- a/src/text-filter/internal.tsx +++ b/src/text-filter/internal.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx'; import { useUniqueId } from '@cloudscape-design/component-toolkit/internal'; -import InternalInput from '../input/internal'; +import InternalInput, { StyleClassNames as InputStyleClassNames } from '../input/internal'; import { getBaseProps } from '../internal/base-component'; import { fireNonCancelableEvent } from '../internal/events'; import useForwardFocus from '../internal/hooks/forward-focus'; @@ -19,7 +19,13 @@ import useDebounceSearchResultCallback from './use-debounce-search-result-callba import styles from './styles.css.js'; -type InternalTextFilterProps = TextFilterProps & InternalBaseComponentProps; +type InternalTextFilterProps = TextFilterProps & InternalBaseComponentProps & { styleClassNames?: StyleClassNames }; + +// Style API v2 +interface StyleClassNames { + input?: InputStyleClassNames; + countText?: string; +} const InternalTextFilter = React.forwardRef( ( @@ -38,6 +44,7 @@ const InternalTextFilter = React.forwardRef( onDelayedChange, loading = false, style, + styleClassNames, __internalRootRef, ...rest }: InternalTextFilterProps, @@ -81,9 +88,15 @@ const InternalTextFilter = React.forwardRef( onChange={event => fireNonCancelableEvent(onChange, { filteringText: event.detail.value })} __onDelayedInput={event => fireNonCancelableEvent(onDelayedChange, { filteringText: event.detail.value })} style={style} + styleClassNames={styleClassNames?.input} /> {showResults ? ( - + {countText} ) : null} diff --git a/src/text-filter/search-results.tsx b/src/text-filter/search-results.tsx index 3a2c92bff92..f37b065935b 100644 --- a/src/text-filter/search-results.tsx +++ b/src/text-filter/search-results.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useImperativeHandle, useRef } from 'react'; +import clsx from 'clsx'; import InternalLiveRegion, { InternalLiveRegionRef } from '../live-region/internal'; @@ -14,10 +15,11 @@ interface SearchResultsProps { id: string; children: string; renderLiveRegion: boolean; + className?: string; } export const SearchResults = React.forwardRef( - ({ id, renderLiveRegion, children }: SearchResultsProps, ref?: React.Ref) => { + ({ id, renderLiveRegion, children, className }: SearchResultsProps, ref?: React.Ref) => { const liveRegionRef = useRef(null); useImperativeHandle( @@ -32,7 +34,7 @@ export const SearchResults = React.forwardRef( return ( <> - + {children} {renderLiveRegion && ( diff --git a/src/text-filter/styles.scss b/src/text-filter/styles.scss index 750ffc1e091..86e5f458184 100644 --- a/src/text-filter/styles.scss +++ b/src/text-filter/styles.scss @@ -5,6 +5,17 @@ @use '../internal/styles/index' as styles; @use '../internal/styles/tokens' as awsui; +@use '../internal/styles/style-api' as style-api; + +@include style-api.docs-forward('input', 'input'); +@include style-api.docs( + 'countText', + $properties: (color, font-family, font-size, font-weight), + $descriptions: ( + slot: + 'The results count rendered after the input. It is shown while `filteringText` and `countText` are set and the filter is not disabled.' + ) +); .root { @include styles.styles-reset; diff --git a/src/time-input/__tests__/style-api.test.tsx b/src/time-input/__tests__/style-api.test.tsx new file mode 100644 index 00000000000..b5cd10e1dac --- /dev/null +++ b/src/time-input/__tests__/style-api.test.tsx @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import { render } from '@testing-library/react'; + +import createWrapper from '../../../lib/components/test-utils/dom'; +import TimeInput, { TimeInputProps } from '../../../lib/components/time-input'; + +function renderTimeInput( + props: Omit & { + value?: string; + styleClassNames?: Record>; + } = {} +) { + const { container } = render( {}} ariaLabel="Time" {...props} />); + return { container, wrapper: createWrapper(container).findTimeInput()! }; +} + +describe('Time input Style API v2', () => { + test('forwards the input classes to the Input slots and ignores unknown keys', () => { + const { container, wrapper } = renderTimeInput({ + value: '14:30:00', + styleClassNames: { input: { root: 'a', input: 'b', unknown: 'c' }, unknown: 'd' }, + }); + const nativeInput = wrapper.findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + expect(container.querySelector('.c')).toBeNull(); + expect(container.querySelector('.d')).toBeNull(); + }); + + test('forwards the input classes in every state', () => { + for (const state of [{}, { invalid: true }, { warning: true }, { disabled: true }, { readOnly: true }]) { + const { wrapper } = renderTimeInput({ ...state, styleClassNames: { input: { root: 'a', input: 'b' } } }); + const nativeInput = wrapper.findNativeInput().getElement(); + expect(nativeInput.parentElement).toHaveClass('a'); + expect(nativeInput).toHaveClass('b'); + } + }); + + test('does not leak the styleClassNames prop to the DOM', () => { + const { container } = renderTimeInput({ styleClassNames: { input: { root: 'a', input: 'b' } } }); + expect(container.querySelector('[styleClassNames]')).toBeNull(); + }); +}); diff --git a/src/time-input/internal.tsx b/src/time-input/internal.tsx index e575416851e..f0ea945a20f 100644 --- a/src/time-input/internal.tsx +++ b/src/time-input/internal.tsx @@ -3,6 +3,7 @@ import React, { Ref, useRef } from 'react'; import clsx from 'clsx'; +import { StyleClassNames as InputStyleClassNames } from '../input/internal'; import MaskedInput from '../internal/components/masked-input'; import { MaskArgs } from '../internal/components/masked-input/utils/mask-format'; import useForwardFocus from '../internal/hooks/forward-focus'; @@ -24,13 +25,19 @@ const getMaskArgs = (format: string, use24Hour: boolean): MaskArgs => { }; }; -type InternalTimeInputProps = TimeInputProps & InternalBaseComponentProps; +type InternalTimeInputProps = TimeInputProps & InternalBaseComponentProps & { styleClassNames?: StyleClassNames }; + +// Style API v2 +interface StyleClassNames { + input?: InputStyleClassNames; +} const InternalTimeInput = React.forwardRef( ( { format = 'hh:mm:ss', use24Hour = true, autoComplete = true, __internalRootRef, ...props }: InternalTimeInputProps, ref: Ref ) => { + const { styleClassNames, ...restProps } = props; const inputRef = useRef(null); useForwardFocus(ref, inputRef); @@ -38,13 +45,14 @@ const InternalTimeInput = React.forwardRef( return ( ); } diff --git a/src/time-input/styles.scss b/src/time-input/styles.scss index 5a54f6dcc39..6d57ab777ef 100644 --- a/src/time-input/styles.scss +++ b/src/time-input/styles.scss @@ -3,6 +3,10 @@ SPDX-License-Identifier: Apache-2.0 */ +@use '../internal/styles/style-api' as style-api; + +@include style-api.docs-forward('input', 'input'); + .root { /* used in test-utils */ }