From f7857a832933ad12886f8dea5c91fcc94f07ffca Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 24 Oct 2024 14:59:42 -0700 Subject: [PATCH 01/24] feat: Adds internal file token group --- .../file-upload/scenario-standalone.page.tsx | 1 + src/file-upload/interfaces.ts | 6 + src/file-upload/internal.tsx | 88 +++++----- .../__tests__/default-formatters.test.tsx | 32 ++++ .../__tests__/file-token-group.test.tsx | 2 + .../file-token-group/default-formatters.ts | 26 +++ .../file-token-group/file-token.tsx | 154 ++++++++++++++++++ .../components/file-token-group/index.tsx | 89 ++++++++++ .../components/file-token-group/interfaces.ts | 100 ++++++++++++ .../components/file-token-group/styles.scss | 65 ++++++++ .../file-token-group/test-classes/styles.scss | 11 ++ .../components/file-token-group/thumbnail.tsx | 41 +++++ src/internal/components/token-list/index.tsx | 10 +- .../components/token-list/interfaces.ts | 1 + .../components/token-list/styles.scss | 8 +- src/token-group/dismiss-button.tsx | 7 +- src/token-group/styles.scss | 12 +- src/token-group/token.tsx | 12 +- 18 files changed, 614 insertions(+), 51 deletions(-) create mode 100644 src/internal/components/file-token-group/__tests__/default-formatters.test.tsx create mode 100644 src/internal/components/file-token-group/__tests__/file-token-group.test.tsx create mode 100644 src/internal/components/file-token-group/default-formatters.ts create mode 100644 src/internal/components/file-token-group/file-token.tsx create mode 100644 src/internal/components/file-token-group/index.tsx create mode 100644 src/internal/components/file-token-group/interfaces.ts create mode 100644 src/internal/components/file-token-group/styles.scss create mode 100644 src/internal/components/file-token-group/test-classes/styles.scss create mode 100644 src/internal/components/file-token-group/thumbnail.tsx diff --git a/pages/file-upload/scenario-standalone.page.tsx b/pages/file-upload/scenario-standalone.page.tsx index 66ed99a5ee..bd2dc7a562 100644 --- a/pages/file-upload/scenario-standalone.page.tsx +++ b/pages/file-upload/scenario-standalone.page.tsx @@ -45,6 +45,7 @@ export default function FileUploadScenarioStandalone() { description={acceptMultiple ? 'Upload your contract with all amendments' : 'Upload your contract'} > ; + /** + * Alignment of the file tokens. Defaults to "vertical". + */ + fileTokenAlignment?: FileUploadProps.FileTokenAlignment; /** * An object containing all the localized strings required by the component: * * `uploadButtonText` (function): A function to render the text of the file upload button. It takes `multiple` attribute to define plurality. @@ -89,6 +93,8 @@ export namespace FileUploadProps { file: File; } + export type FileTokenAlignment = 'vertical' | 'horizontal'; + export interface I18nStrings { uploadButtonText: (multiple: boolean) => string; dropzoneText: (multiple: boolean) => string; diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index 0af85798e1..b438941f0e 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -13,7 +13,9 @@ import { ConstraintText, FormFieldError, FormFieldWarning } from '../form-field/ import { getBaseProps } from '../internal/base-component'; import InternalFileDropzone, { useFilesDragging } from '../internal/components/file-dropzone'; import InternalFileInput from '../internal/components/file-input'; -import TokenList from '../internal/components/token-list'; +import InternalFileTokenGroup from '../internal/components/file-token-group'; +import * as defaultFormatters from '../internal/components/file-token-group/default-formatters'; +import InternalFileToken from '../internal/components/file-token-group/file-token'; import { fireNonCancelableEvent } from '../internal/events'; import checkControlled from '../internal/hooks/check-controlled'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; @@ -22,8 +24,6 @@ import { useMergeRefs } from '../internal/hooks/use-merge-refs'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import { joinStrings } from '../internal/utils/strings'; import InternalSpaceBetween from '../space-between/internal'; -import { Token } from '../token-group/token'; -import { FileOption } from './file-option'; import { FileUploadProps } from './interfaces'; import fileInputStyles from '../internal/components/file-input/styles.css.js'; @@ -52,6 +52,7 @@ function InternalFileUpload( warningText, fileErrors, fileWarnings, + fileTokenAlignment = 'vertical', ...restProps }: InternalFileUploadProps, externalRef: ForwardedRef @@ -78,6 +79,9 @@ function InternalFileUpload( const fileInputRef = useRef(null); const ref = useMergeRefs(fileInputRef, externalRef); + const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; + const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; + checkControlled('FileUpload', 'value', value, 'onChange', onChange); if (!multiple && value.length > 1) { @@ -165,48 +169,48 @@ function InternalFileUpload( {!multiple && value.length > 0 ? ( - - onFileRemove(0)} - errorText={fileErrors?.[0]} - warningText={fileWarnings?.[0]} - errorIconAriaLabel={i18nStrings.errorIconAriaLabel} - warningIconAriaLabel={i18nStrings.warningIconAriaLabel} - data-index={0} - > - - - + onFileRemove(0)} + i18nStrings={{ + removeFileAriaLabel: () => i18nStrings.removeFileAriaLabel(0), + errorIconAriaLabel: i18nStrings.errorIconAriaLabel, + warningIconAriaLabel: i18nStrings.warningIconAriaLabel, + formatFileLastModified: date => formatFileLastModified(date), + formatFileSize: size => formatFileSize(size), + }} + index={0} + /> ) : null} {multiple && value.length > 0 ? ( - - ( - onFileRemove(fileIndex)} - errorText={fileErrors?.[fileIndex]} - warningText={fileWarnings?.[fileIndex]} - errorIconAriaLabel={i18nStrings.errorIconAriaLabel} - warningIconAriaLabel={i18nStrings.warningIconAriaLabel} - data-index={fileIndex} - > - - - )} - limit={tokenLimit} - i18nStrings={{ - limitShowFewer: i18nStrings.limitShowFewer, - limitShowMore: i18nStrings.limitShowMore, - }} - /> - + ({ + file, + errorText: fileErrors?.[fileIndex], + warningText: fileWarnings?.[fileIndex], + }))} + showFileLastModified={metadata.showFileLastModified} + showFileSize={metadata.showFileSize} + showFileThumbnail={metadata.showFileThumbnail} + i18nStrings={{ + removeFileAriaLabel: index => i18nStrings.removeFileAriaLabel(index), + errorIconAriaLabel: i18nStrings.errorIconAriaLabel, + warningIconAriaLabel: i18nStrings.warningIconAriaLabel, + formatFileLastModified: date => formatFileLastModified(date), + formatFileSize: size => formatFileSize(size), + limitShowMore: i18nStrings.limitShowMore, + limitShowFewer: i18nStrings.limitShowFewer, + }} + onDismiss={event => onFileRemove(event.detail.fileIndex)} + /> ) : null} ); diff --git a/src/internal/components/file-token-group/__tests__/default-formatters.test.tsx b/src/internal/components/file-token-group/__tests__/default-formatters.test.tsx new file mode 100644 index 0000000000..7459d7a52d --- /dev/null +++ b/src/internal/components/file-token-group/__tests__/default-formatters.test.tsx @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { formatFileSize } from '../default-formatters'; + +describe('file upload default formatters', () => { + test('rounds file size to KB', () => { + expect(formatFileSize(0)).toBe('0.00 KB'); + expect(formatFileSize(10)).toBe('0.01 KB'); + expect(formatFileSize(100)).toBe('0.10 KB'); + expect(formatFileSize(1000)).toBe('1.00 KB'); + expect(formatFileSize(10_000)).toBe('10.00 KB'); + expect(formatFileSize(100_000)).toBe('100.00 KB'); + expect(formatFileSize(1_000_000 - 6)).toBe('999.99 KB'); + expect(formatFileSize(1_000_000 - 5)).toBe('1000.00 KB'); + }); + + test('rounds file size to MB', () => { + expect(formatFileSize(1_000_000)).toBe('1.00 MB'); + expect(formatFileSize(1_000_000 + 5001)).toBe('1.01 MB'); + expect(formatFileSize(1_000_000_000 - 5001)).toBe('999.99 MB'); + expect(formatFileSize(1_000_000_000 - 5000)).toBe('1000.00 MB'); + }); + + test('rounds file size to GB', () => { + expect(formatFileSize(1_000_000_000)).toBe('1.00 GB'); + }); + + test('rounds file size to TB', () => { + expect(formatFileSize(1_000_000_000_000)).toBe('1.00 TB'); + }); +}); diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx new file mode 100644 index 0000000000..cf1406c942 --- /dev/null +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -0,0 +1,2 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 diff --git a/src/internal/components/file-token-group/default-formatters.ts b/src/internal/components/file-token-group/default-formatters.ts new file mode 100644 index 0000000000..3fffd6538b --- /dev/null +++ b/src/internal/components/file-token-group/default-formatters.ts @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { formatDateTime } from '../../utils/date-time'; + +const KB = 1000; +const MB = 1000 ** 2; +const GB = 1000 ** 3; +const TB = 1000 ** 4; + +export function formatFileSize(size: number): string { + if (size < MB) { + return `${(size / KB).toFixed(2)} KB`; + } + if (size < GB) { + return `${(size / MB).toFixed(2)} MB`; + } + if (size < TB) { + return `${(size / GB).toFixed(2)} GB`; + } + return `${(size / TB).toFixed(2)} TB`; +} + +export function formatFileLastModified(date: Date): string { + return formatDateTime(date); +} diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx new file mode 100644 index 0000000000..a87e243d14 --- /dev/null +++ b/src/internal/components/file-token-group/file-token.tsx @@ -0,0 +1,154 @@ +// 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 InternalBox from '../../../box/internal.js'; +import InternalLiveRegion from '../../../live-region/internal'; +import InternalSpaceBetween from '../../../space-between/internal.js'; +import InternalSpinner from '../../../spinner/internal.js'; +import { TokenGroupProps } from '../../../token-group/interfaces.js'; +import { Token } from '../../../token-group/token.js'; +import { BaseComponentProps } from '../../base-component/index.js'; +import Tooltip from '../tooltip/index'; +import * as defaultFormatters from './default-formatters.js'; +import { FileOptionThumbnail } from './thumbnail.js'; + +import styles from './styles.css.js'; +import testUtilStyles from './test-classes/styles.css.js'; + +export namespace FileTokenProps { + export interface I18nStrings { + removeFileAriaLabel: (fileIndex: number) => string; + errorIconAriaLabel?: string; + warningIconAriaLabel?: string; + formatFileSize?: (sizeInBytes: number) => string; + formatFileLastModified?: (date: Date) => string; + } +} + +export interface FileTokenProps extends BaseComponentProps { + file: File; + onDismiss: () => void; + showFileSize?: boolean; + showFileLastModified?: boolean; + showFileThumbnail?: boolean; + errorText?: React.ReactNode; + warningText?: React.ReactNode; + loading?: boolean; + loadingText?: string; + i18nStrings: FileTokenProps.I18nStrings; + disabled?: boolean; + dismissLabel?: string; + alignment?: TokenGroupProps.Alignment; + groupContainsImage?: boolean; + index: number; +} + +function InternalFileToken({ + file, + showFileLastModified, + showFileSize, + showFileThumbnail, + i18nStrings, + onDismiss, + errorText, + warningText, + disabled, + loading, + loadingText, + alignment, + groupContainsImage, + index, +}: FileTokenProps) { + const isImage = file.type.startsWith('image/'); + const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; + const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; + + const containerRef = React.useRef(null); + const [showTooltip, setShowTooltip] = useState(false); + const [imageError, setImageError] = useState(false); + + return ( +
setShowTooltip(true)} + onBlur={() => setShowTooltip(false)} + > + {loading && ( + <> +
+
+ +
+ + )} + + + {showFileThumbnail && isImage && } + +
+ +
setShowTooltip(true)} onMouseOut={() => setShowTooltip(false)}> + + {file.name} + +
+ + {showFileSize && file.size ? ( + + {formatFileSize(file.size)} + + ) : null} + + {showFileLastModified && file.lastModified ? ( + + {formatFileLastModified(new Date(file.lastModified))} + + ) : null} +
+
+
+
+ {alignment === 'horizontal' && showTooltip && ( + {file.name}} + /> + )} + {loading && loadingText && {loadingText}} +
+ ); +} + +export default InternalFileToken; diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx new file mode 100644 index 0000000000..08b537479f --- /dev/null +++ b/src/internal/components/file-token-group/index.tsx @@ -0,0 +1,89 @@ +// 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 { getBaseProps } from '../../base-component/index.js'; +import { fireNonCancelableEvent } from '../../events/index.js'; +import { InternalBaseComponentProps } from '../../hooks/use-base-component/index.js'; +import { useListFocusController } from '../../hooks/use-list-focus-controller.js'; +import { useMergeRefs } from '../../hooks/use-merge-refs/index.js'; +import TokenList from '../token-list/index.js'; +import InternalFileToken from './file-token.js'; +import { FileTokenGroupProps } from './interfaces.js'; + +import tokenListStyles from '../token-list/styles.css.js'; +import styles from './styles.css.js'; +import testStyles from './test-classes/styles.css.js'; + +type InternalFileTokenGroupProps = FileTokenGroupProps & InternalBaseComponentProps; + +function InternalFileTokenGroup({ + items, + showFileLastModified, + showFileSize, + showFileThumbnail, + i18nStrings, + onDismiss, + limit, + alignment = 'vertical', + __internalRootRef, + ...restProps +}: InternalFileTokenGroupProps) { + const baseProps = getBaseProps(restProps); + + const [nextFocusIndex, setNextFocusIndex] = useState(null); + const tokenListRef = useListFocusController({ + nextFocusIndex, + onFocusMoved: target => { + target.focus(); + setNextFocusIndex(null); + }, + listItemSelector: `.${tokenListStyles['list-item']}`, + showMoreSelector: `.${tokenListStyles.toggle}`, + }); + + const mergedRef = useMergeRefs(__internalRootRef, tokenListRef); + + const isImage = (file: File) => file.type.startsWith('image/'); + const groupContainsImage = items.filter(item => isImage(item.file)).length > 0; + + return ( +
+ ( + { + fireNonCancelableEvent(onDismiss, { fileIndex }); + setNextFocusIndex(fileIndex); + }} + errorText={file.errorText} + warningText={file.warningText} + i18nStrings={i18nStrings} + disabled={file.disabled} + loading={file.loading} + loadingText={file.loadingText} + alignment={alignment} + groupContainsImage={groupContainsImage} + index={fileIndex} + /> + )} + limit={limit} + i18nStrings={{ + limitShowFewer: i18nStrings.limitShowFewer, + limitShowMore: i18nStrings.limitShowMore, + }} + /> +
+ ); +} + +export default InternalFileTokenGroup; diff --git a/src/internal/components/file-token-group/interfaces.ts b/src/internal/components/file-token-group/interfaces.ts new file mode 100644 index 0000000000..fcdad2a21d --- /dev/null +++ b/src/internal/components/file-token-group/interfaces.ts @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BaseComponentProps } from '../../base-component'; +import { NonCancelableEventHandler } from '../../events'; + +export interface FileTokenGroupProps extends BaseComponentProps { + /** + * Show file size in the token. Use `i18nStrings.formatFileSize` to customize it. + */ + showFileSize?: boolean; + + /** + * Show file last modified timestamp in the token. Use `i18nStrings.formatFileLastModified` to customize it. + */ + showFileLastModified?: boolean; + + /** + * Show file thumbnail in the token. Only supported for images. + */ + showFileThumbnail?: boolean; + /** + * Called when the user clicks on the dismiss button. The token won't be automatically removed. + * Make sure that you add a listener to this event to update your application state. + */ + onDismiss: NonCancelableEventHandler; + + /** + * Specifies the maximum number of displayed tokens. If the property isn't set, all of the tokens are displayed. + */ + limit?: number; + /** + * Specifies the direction in which tokens are aligned (`horizontal | vertical`). + */ + alignment?: FileTokenGroupProps.Alignment; + + /** + * + * An array of objects representing token items. Each token has the following properties: + * + * - `file` (string) - File value. + * - `disabled` [boolean] - (Optional) Determines whether the token is disabled. + * - `loading` (boolean) - (Optional) Custom SVG icon. Equivalent to the `svg` slot of the [icon component](/components/icon/). + * - `loadingText` (string) - (Optional) Specifies the text that screen reader announces when the button is in a loading state. + * - `errorText` (string) - (Optional) Text that displays as a validation error message. + * - `warningText` (string) - (Optional) - Text that displays as a validation warning message. + */ + items: ReadonlyArray; + /** + * Adds an `aria-label` to the "Show fewer" button. + * Use to assign unique labels when there are multiple file token groups with the same `limitShowFewer` label on one page. + */ + limitShowFewerAriaLabel?: string; + /** + * Adds an `aria-label` to the "Show more" button. + * Use to assign unique labels when there are multiple file token groups with the same `limitShowMore` label on one page. + */ + limitShowMoreAriaLabel?: string; + /** + * Specifies if the control is read-only, which prevents the + * user from modifying the value. A read-only control is still focusable. + */ + readOnly?: boolean; + /** + * An object containing all the localized strings required by the component: + * * `removeFileAriaLabel` (function): A function to render the ARIA label for file token remove button. + * * `errorIconAriaLabel` (string): The ARIA label to be shown on the error file icon. + * * `warningIconAriaLabel` (string): The ARIA label to be shown on the warning file icon. + * * `formatFileSize` (function): (Optional) A function that takes file size in bytes, and produces a formatted string. + * * `formatFileLastModified` (function): (Optional) A function that takes the files last modified date, and produces a formatted string. + */ + i18nStrings: FileTokenGroupProps.I18nStrings; +} + +export namespace FileTokenGroupProps { + export interface DismissDetail { + fileIndex: number; + } + + export interface I18nStrings { + limitShowFewer?: string; + limitShowMore?: string; + + removeFileAriaLabel: (fileIndex: number) => string; + errorIconAriaLabel?: string; + warningIconAriaLabel?: string; + formatFileSize?: (sizeInBytes: number) => string; + formatFileLastModified?: (date: Date) => string; + } + + export type Alignment = 'horizontal' | 'vertical'; + + export interface Item { + file: File; + disabled?: boolean; + loading?: boolean; + loadingText?: string; + errorText?: null | string; + warningText?: null | string; + } +} diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss new file mode 100644 index 0000000000..6099dc36ca --- /dev/null +++ b/src/internal/components/file-token-group/styles.scss @@ -0,0 +1,65 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '../../styles/tokens' as awsui; +@use '../../styles' as styles; + +$image-size: 48px; + +.root { + @include styles.styles-reset(); +} + +.file-token { + block-size: 100%; + position: relative; +} + +.file-loading-overlay { + position: absolute; + inline-size: 100%; + block-size: 100%; + background: awsui.$color-background-container-content; + opacity: 75%; + + &-spinner { + position: absolute; + inset-inline-start: calc(50% - 8px); + inset-block-end: calc(50% - 8px); + } +} + +.file-option-name, +.file-option-size, +.file-option-last-modified { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.file-option { + inline-size: 100%; + min-inline-size: 0; + display: flex; + gap: awsui.$space-scaled-xs; +} + +.file-option-thumbnail { + margin-block-start: awsui.$space-static-xxs; +} + +.file-option-thumbnail-image { + inline-size: $image-size; + block-size: $image-size; + object-fit: cover; +} + +.file-option-metadata { + inline-size: 100%; + + &.with-image { + inline-size: calc(100% - $image-size); + } +} diff --git a/src/internal/components/file-token-group/test-classes/styles.scss b/src/internal/components/file-token-group/test-classes/styles.scss new file mode 100644 index 0000000000..eae0803276 --- /dev/null +++ b/src/internal/components/file-token-group/test-classes/styles.scss @@ -0,0 +1,11 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +.root, +.file-option-thumbnail, +.file-option-name, +.file-option-size, +.file-option-last-modified { + /* used in test-utils */ +} diff --git a/src/internal/components/file-token-group/thumbnail.tsx b/src/internal/components/file-token-group/thumbnail.tsx new file mode 100644 index 0000000000..b81acedc74 --- /dev/null +++ b/src/internal/components/file-token-group/thumbnail.tsx @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useEffect, useState } from 'react'; + +import styles from './styles.css.js'; + +interface FileOptionThumbnailProps { + file: File; + setHasError: (hasError: boolean) => void; +} + +export function FileOptionThumbnail({ file, setHasError }: FileOptionThumbnailProps) { + const [imageSrc, setImageSrc] = useState(''); + + useEffect(() => { + // The URL.createObjectURL is not available in jsdom. + if (URL.createObjectURL) { + const src = URL.createObjectURL(file); + setImageSrc(src); + + return () => { + URL.revokeObjectURL(src); + }; + } + }, [file]); + + return ( +
+ {file.name} { + setHasError(true); + currentTarget.onerror = null; // prevents looping + }} + /> +
+ ); +} diff --git a/src/internal/components/token-list/index.tsx b/src/internal/components/token-list/index.tsx index 6ace0cfdb0..c92f8791e7 100644 --- a/src/internal/components/token-list/index.tsx +++ b/src/internal/components/token-list/index.tsx @@ -21,6 +21,7 @@ export default function TokenList({ i18nStrings, limitShowFewerAriaLabel, limitShowMoreAriaLabel, + isGrid = false, onExpandedClick = () => undefined, }: TokenListProps) { const controlId = useUniqueId(); @@ -77,7 +78,14 @@ export default function TokenList({ return (
{hasVisibleItems && ( -
    +
      {visibleItems.map((item, itemIndex) => (
    • { onExpandedClick?: (isExpanded: boolean) => void; limitShowFewerAriaLabel?: string; limitShowMoreAriaLabel?: string; + isGrid?: boolean; } export interface I18nStrings { diff --git a/src/internal/components/token-list/styles.scss b/src/internal/components/token-list/styles.scss index 536b6e9baa..4c3a77e71e 100644 --- a/src/internal/components/token-list/styles.scss +++ b/src/internal/components/token-list/styles.scss @@ -8,15 +8,16 @@ @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; .root { - display: flex; gap: awsui.$space-scaled-xs; &.horizontal { + display: flex; gap: awsui.$space-xs; flex-direction: row; flex-wrap: wrap; } &.vertical { + display: flex; flex-direction: column; } } @@ -41,6 +42,11 @@ &.vertical { flex-direction: column; } + &.grid { + display: grid; + gap: awsui.$space-xs; + grid-template-columns: repeat(auto-fill, 220px); + } } .list-item { diff --git a/src/token-group/dismiss-button.tsx b/src/token-group/dismiss-button.tsx index 91037853f6..72c74a8762 100644 --- a/src/token-group/dismiss-button.tsx +++ b/src/token-group/dismiss-button.tsx @@ -5,6 +5,7 @@ import React, { forwardRef, Ref } from 'react'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; import InternalIcon from '../icon/internal'; +import InternalSpinner from '../spinner/internal'; import { GeneratedAnalyticsMetadataTokenGroupDismiss } from './analytics-metadata/interfaces'; import styles from './styles.css.js'; @@ -12,6 +13,7 @@ import styles from './styles.css.js'; interface DismissButtonProps { disabled?: boolean; readOnly?: boolean; + loading?: boolean; onDismiss?: () => void; dismissLabel?: string; } @@ -19,7 +21,7 @@ interface DismissButtonProps { export default forwardRef(DismissButton); function DismissButton( - { disabled, dismissLabel, onDismiss, readOnly }: DismissButtonProps, + { disabled, dismissLabel, onDismiss, readOnly, loading }: DismissButtonProps, ref: Ref ) { const analyticsMetadata: GeneratedAnalyticsMetadataTokenGroupDismiss = { @@ -38,13 +40,12 @@ function DismissButton( if (disabled || readOnly || !onDismiss) { return; } - onDismiss(); }} aria-label={dismissLabel} {...(disabled || readOnly ? {} : getAnalyticsMetadataAttribute(analyticsMetadata))} > - + {loading ? : } ); } diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss index b088c34313..8b8ed7c9e0 100644 --- a/src/token-group/styles.scss +++ b/src/token-group/styles.scss @@ -45,9 +45,13 @@ .token { block-size: 100%; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: max-content auto; gap: awsui.$space-xxs; + + &-contains-image { + grid-template-rows: 70px auto; + } } .token-box { @@ -67,6 +71,10 @@ border-end-end-radius: awsui.$border-radius-token; color: awsui.$color-text-body-default; box-sizing: border-box; + + &.horizontal { + max-inline-size: 220px; + } } @mixin token-box-validation { border-inline-start-width: awsui.$border-invalid-width; diff --git a/src/token-group/token.tsx b/src/token-group/token.tsx index b8f12c22f9..2c4d62abe9 100644 --- a/src/token-group/token.tsx +++ b/src/token-group/token.tsx @@ -8,6 +8,7 @@ import { FormFieldError, FormFieldWarning } from '../form-field/internal'; import { getBaseProps } from '../internal/base-component'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import DismissButton from './dismiss-button'; +import { TokenGroupProps } from './interfaces'; import styles from './styles.css.js'; @@ -22,6 +23,8 @@ interface TokenProps { errorIconAriaLabel?: string; warningText?: React.ReactNode; warningIconAriaLabel?: string; + alignment?: TokenGroupProps.Alignment; + groupContainsImage?: boolean; className?: string; } @@ -36,6 +39,8 @@ export function Token({ warningText, errorIconAriaLabel, warningIconAriaLabel, + alignment, + groupContainsImage, ...restProps }: TokenProps) { const errorId = useUniqueId('error'); @@ -47,7 +52,9 @@ export function Token({ return (
      {children} From 6cb25955e0f3add24e7236128f4223b083dc5ab0 Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 24 Oct 2024 15:01:06 -0700 Subject: [PATCH 02/24] chore: Removes unused files --- .../__tests__/default-formatters.test.ts | 32 ------------- src/file-upload/default-formatters.ts | 26 ---------- src/file-upload/file-option/index.tsx | 47 ------------------- src/file-upload/file-option/styles.scss | 34 -------------- src/file-upload/file-option/thumbnail.tsx | 32 ------------- 5 files changed, 171 deletions(-) delete mode 100644 src/file-upload/__tests__/default-formatters.test.ts delete mode 100644 src/file-upload/default-formatters.ts delete mode 100644 src/file-upload/file-option/index.tsx delete mode 100644 src/file-upload/file-option/styles.scss delete mode 100644 src/file-upload/file-option/thumbnail.tsx diff --git a/src/file-upload/__tests__/default-formatters.test.ts b/src/file-upload/__tests__/default-formatters.test.ts deleted file mode 100644 index 7459d7a52d..0000000000 --- a/src/file-upload/__tests__/default-formatters.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { formatFileSize } from '../default-formatters'; - -describe('file upload default formatters', () => { - test('rounds file size to KB', () => { - expect(formatFileSize(0)).toBe('0.00 KB'); - expect(formatFileSize(10)).toBe('0.01 KB'); - expect(formatFileSize(100)).toBe('0.10 KB'); - expect(formatFileSize(1000)).toBe('1.00 KB'); - expect(formatFileSize(10_000)).toBe('10.00 KB'); - expect(formatFileSize(100_000)).toBe('100.00 KB'); - expect(formatFileSize(1_000_000 - 6)).toBe('999.99 KB'); - expect(formatFileSize(1_000_000 - 5)).toBe('1000.00 KB'); - }); - - test('rounds file size to MB', () => { - expect(formatFileSize(1_000_000)).toBe('1.00 MB'); - expect(formatFileSize(1_000_000 + 5001)).toBe('1.01 MB'); - expect(formatFileSize(1_000_000_000 - 5001)).toBe('999.99 MB'); - expect(formatFileSize(1_000_000_000 - 5000)).toBe('1000.00 MB'); - }); - - test('rounds file size to GB', () => { - expect(formatFileSize(1_000_000_000)).toBe('1.00 GB'); - }); - - test('rounds file size to TB', () => { - expect(formatFileSize(1_000_000_000_000)).toBe('1.00 TB'); - }); -}); diff --git a/src/file-upload/default-formatters.ts b/src/file-upload/default-formatters.ts deleted file mode 100644 index 1374abc65b..0000000000 --- a/src/file-upload/default-formatters.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { formatDateTime } from '../internal/utils/date-time'; - -const KB = 1000; -const MB = 1000 ** 2; -const GB = 1000 ** 3; -const TB = 1000 ** 4; - -export function formatFileSize(size: number): string { - if (size < MB) { - return `${(size / KB).toFixed(2)} KB`; - } - if (size < GB) { - return `${(size / MB).toFixed(2)} MB`; - } - if (size < TB) { - return `${(size / GB).toFixed(2)} GB`; - } - return `${(size / TB).toFixed(2)} TB`; -} - -export function formatFileLastModified(date: Date): string { - return formatDateTime(date); -} diff --git a/src/file-upload/file-option/index.tsx b/src/file-upload/file-option/index.tsx deleted file mode 100644 index ee36f07987..0000000000 --- a/src/file-upload/file-option/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; - -import InternalBox from '../../box/internal'; -import InternalSpaceBetween from '../../space-between/internal'; -import * as defaultFormatters from '../default-formatters'; -import { FileMetadata, FileUploadProps } from '../interfaces'; -import { FileOptionThumbnail } from './thumbnail'; - -import styles from './styles.css.js'; - -interface FileOptionProps { - file: File; - metadata: FileMetadata; - i18nStrings: FileUploadProps.I18nStrings; -} - -export function FileOption({ file, metadata, i18nStrings }: FileOptionProps) { - const isImage = file.type.startsWith('image/'); - const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; - const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; - return ( - - {metadata.showFileThumbnail && isImage && } - -
      - - {file.name} - - {metadata.showFileSize && file.size ? ( - - {formatFileSize(file.size)} - - ) : null} - - {metadata.showFileLastModified && file.lastModified ? ( - - {formatFileLastModified(new Date(file.lastModified))} - - ) : null} - -
      -
      - ); -} diff --git a/src/file-upload/file-option/styles.scss b/src/file-upload/file-option/styles.scss deleted file mode 100644 index dce4393930..0000000000 --- a/src/file-upload/file-option/styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '../../internal/styles/tokens' as awsui; - -.file-option-name, -.file-option-size, -.file-option-last-modified, -.file-option-thumbnail { - /* used in test-utils */ -} - -.file-option { - inline-size: 100%; - min-inline-size: 0; - display: flex; - gap: awsui.$space-scaled-xs; -} - -.file-option-thumbnail { - margin-block-start: awsui.$space-static-xxs; - max-inline-size: 60px; -} - -.file-option-thumbnail-image { - inline-size: 100%; - block-size: auto; -} - -.file-option-metadata { - inline-size: 100%; -} diff --git a/src/file-upload/file-option/thumbnail.tsx b/src/file-upload/file-option/thumbnail.tsx deleted file mode 100644 index 86424be0ce..0000000000 --- a/src/file-upload/file-option/thumbnail.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import React, { useEffect, useState } from 'react'; - -import styles from './styles.css.js'; - -interface FileOptionThumbnailProps { - file: File; -} - -export function FileOptionThumbnail({ file }: FileOptionThumbnailProps) { - const [imageSrc, setImageSrc] = useState(''); - - useEffect(() => { - // The URL.createObjectURL is not available in jsdom. - if (URL.createObjectURL) { - const src = URL.createObjectURL(file); - setImageSrc(src); - - return () => { - URL.revokeObjectURL(src); - }; - } - }, [file]); - - return ( -
      - {file.name} -
      - ); -} From 3c29a4dc4a6af02113270e56f04ce886c7e87cc8 Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 28 Oct 2024 17:55:06 -0700 Subject: [PATCH 03/24] chore: Updates tests --- .../__tests__/file-token-group.test.tsx | 295 ++++++++++++++++++ .../components/file-token-group/index.tsx | 2 + src/test-utils/dom/file-upload/index.ts | 32 +- .../dom/internal/file-token-group.ts | 59 ++++ 4 files changed, 357 insertions(+), 31 deletions(-) create mode 100644 src/test-utils/dom/internal/file-token-group.ts diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx index cf1406c942..2046d8a0cb 100644 --- a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -1,2 +1,297 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; +import { render as testingLibraryRender } from '@testing-library/react'; + +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + +import '../../../../__a11y__/to-validate-a11y'; +import FileTokenGroup, { + FileTokenGroupProps, +} from '../../../../../lib/components/internal/components/file-token-group'; +import createWrapper from '../../../../../lib/components/test-utils/dom'; +import FileTokenGroupWrapper from '../../../../../lib/components/test-utils/dom/internal/file-token-group'; + +jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ + ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), + warnOnce: jest.fn(), +})); + +jest.mock('../../../../../lib/components/internal/utils/date-time', () => ({ + formatDateTime: () => '2020-06-01T00:00:00', +})); + +const onDismiss = jest.fn(); + +afterEach(() => { + (warnOnce as jest.Mock).mockReset(); + onDismiss.mockReset(); +}); + +const defaultProps: FileTokenGroupProps = { + items: [], + onDismiss, + i18nStrings: { + removeFileAriaLabel: fileIndex => `Remove file ${fileIndex + 1}`, + errorIconAriaLabel: 'Error', + warningIconAriaLabel: 'Warning', + limitShowFewer: 'Show fewer files', + limitShowMore: 'Show more files', + }, +}; + +const file1 = new File([new Blob(['Test content 1'], { type: 'text/plain' })], 'test-file-1.txt', { + type: 'text/plain', + lastModified: 1590962400000, +}); +const file2 = new File([new Blob(['Test content 2'], { type: 'text/plain' })], 'test-file-2.txt', { + type: 'image/png', + lastModified: 1590962400000, +}); + +function render(props: Partial) { + testingLibraryRender( +
      + +
      Test label
      +
      + ); + const element = createWrapper().findByClassName(FileTokenGroupWrapper.rootSelector)!.getElement(); + return new FileTokenGroupWrapper(element); +} + +function renderStateful(props: Partial = {}) { + testingLibraryRender(); + const element = createWrapper().findByClassName(FileTokenGroupWrapper.rootSelector)!.getElement(); + return new FileTokenGroupWrapper(element); +} + +function StatefulFileTokenGroup({ items: initialItems = [], ...rest }: Partial) { + const [items, setItems] = useState(initialItems); + return ( + setItems(prev => prev.filter((_, index) => index !== event.detail.fileIndex))} + /> + ); +} + +describe('File upload tokens', () => { + test(`renders file tokens`, () => { + const wrapper = render({ items: [{ file: file1 }, { file: file2 }] }); + + expect(wrapper.findFileTokens()).toHaveLength(2); + + expect(wrapper.findFileTokens()[0].getElement()).toHaveTextContent('test-file-1.txt'); + expect(wrapper.findFileToken(1)!.getElement()).toHaveTextContent('test-file-1.txt'); + + expect(wrapper.findFileTokens()[1].getElement()).toHaveTextContent('test-file-2.txt'); + expect(wrapper.findFileToken(2)!.getElement()).toHaveTextContent('test-file-2.txt'); + }); + + test('file token remove button has ARIA label set', () => { + const wrapper = render({ items: [{ file: file1 }] }); + expect(wrapper.findFileToken(1)!.findRemoveButton()!.getElement()).toHaveAccessibleName('Remove file 1'); + }); + + test('selected file can be removed - single', () => { + const wrapperSingular = render({ items: [{ file: file1 }] }); + wrapperSingular.findFileToken(1)!.findRemoveButton()!.click(); + + expect(onDismiss).toHaveBeenCalledWith(expect.objectContaining({ detail: { fileIndex: 0 } })); + }); + + test('selected file can be removed - multiple', () => { + const wrapperMultiple = render({ items: [{ file: file1 }, { file: file2 }] }); + wrapperMultiple.findFileToken(2)!.findRemoveButton()!.click(); + + expect(onDismiss).toHaveBeenCalledWith(expect.objectContaining({ detail: { fileIndex: 1 } })); + }); + + test('file token only shows name by default', () => { + const wrapper = render({ items: [{ file: file1 }] }); + + expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt'); + expect(wrapper.findFileToken(1)!.findFileSize()).toBe(null); + expect(wrapper.findFileToken(1)!.findFileLastModified()).toBe(null); + expect(wrapper.findFileToken(1)!.findFileThumbnail()).toBe(null); + }); + + test('file token metadata can be opt-in', () => { + const wrapper = render({ + items: [{ file: file1 }], + showFileSize: true, + showFileLastModified: true, + }); + expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt'); + expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('0.01 KB'); + expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020-06-01T00:00:00'); + }); + + test('thumbnail is only shown when file type starts with "image"', () => { + const wrapper = render({ items: [{ file: file1 }, { file: file2 }], showFileThumbnail: true }); + expect(wrapper.findFileToken(1)!.findFileThumbnail()).toBe(null); + + expect(wrapper.findFileToken(2)!.findFileThumbnail()).not.toBe(null); + }); + + test('selected file size can be customized', () => { + const wrapper = render({ + items: [{ file: file1 }], + showFileSize: true, + i18nStrings: { + ...defaultProps.i18nStrings, + formatFileSize: sizeInBytes => `${sizeInBytes} bytes`, + }, + }); + expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('14 bytes'); + }); + + test('selected file last update timestamp can be customized', () => { + const wrapper = render({ + items: [{ file: file1 }], + showFileLastModified: true, + i18nStrings: { + ...defaultProps.i18nStrings, + formatFileLastModified: date => `${date.getFullYear()} year`, + }, + }); + expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020 year'); + }); + + test('the `tokenLimit` property controls the number of tokens shown by default', () => { + const wrapper = render({ items: [{ file: file1 }, { file: file2 }], limit: 1 }); + expect(wrapper.findFileTokens()).toHaveLength(1); + expect(wrapper.getElement().textContent).toContain('Show more files'); + }); + + test('file tokens have aria labels set to file names', () => { + const wrapper = render({ items: [{ file: file1 }, { file: file2 }] }); + expect(wrapper.findFileToken(1)!.getElement()).toHaveAttribute('aria-label', file1.name); + expect(wrapper.findFileToken(2)!.getElement()).toHaveAttribute('aria-label', file2.name); + }); + + test('file errors are associated to file tokens', () => { + const wrapper = render({ + items: [ + { file: file1, errorText: 'Error 1' }, + { file: file2, errorText: 'Error 2' }, + ], + }); + expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Error 1'); + expect(wrapper.findFileToken(2)!.getElement()).toHaveAccessibleDescription('Error 2'); + }); + + test('file warnings are associated to file tokens', () => { + const wrapper = render({ + items: [ + { file: file1, warningText: 'Warning 1' }, + { file: file2, warningText: 'Warning 2' }, + ], + }); + expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Warning 1'); + expect(wrapper.findFileToken(2)!.getElement()).toHaveAccessibleDescription('Warning 2'); + }); + + test('file error takes precedence over file warning associated to file tokens', () => { + const wrapper = render({ + items: [{ file: file1, errorText: 'Error 1', warningText: 'Warning 1' }], + }); + expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Error 1'); + expect(wrapper.findFileToken(1)!.getElement()).not.toHaveAccessibleDescription('Warning 1'); + }); +}); + +describe('Focusing behavior', () => { + test.each([1, 2])(`Focus is dispatched to the next token when the token before it is removed, limit=%s`, limit => { + const wrapper = renderStateful({ items: [{ file: file1 }, { file: file2 }], limit }); + wrapper.findFileToken(1)!.findRemoveButton().click(); + + expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus(); + }); + + test('Focus is dispatched to the previous token when removing the token at the end', () => { + const wrapper = renderStateful({ items: [{ file: file1 }, { file: file2 }] }); + wrapper.findFileToken(2)!.findRemoveButton().click(); + + expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus(); + }); + + // test('Focus is dispatched to the file input when the last token is removed, multiple=%s', () => { + // const wrapper = renderStateful({ items: [{ file: file1 }] }); + // wrapper.findFileToken(1)!.findRemoveButton().click(); + + // expect(wrapper.findNativeInput().getElement()).toHaveFocus(); + // }); +}); + +describe('a11y', () => { + test('multiple empty', async () => { + const wrapper = render({ items: [] }); + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('single', async () => { + const wrapper = render({ + items: [{ file: file1 }], + showFileSize: true, + showFileLastModified: true, + }); + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('single w/ errors', async () => { + const wrapper = render({ + items: [{ file: file1, errorText: 'Error' }], + showFileSize: true, + showFileLastModified: true, + }); + + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('single w/ warnings', async () => { + const wrapper = render({ + items: [{ file: file1, warningText: 'Warning' }], + showFileSize: true, + showFileLastModified: true, + }); + + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('multiple w/o errors nor warnings', async () => { + const wrapper = render({ + items: [{ file: file1 }, { file: file2 }], + showFileSize: true, + showFileLastModified: true, + }); + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('multiple w/ errors', async () => { + const wrapper = render({ + items: [ + { file: file1, errorText: 'Error 1' }, + { file: file2, errorText: 'Error 2' }, + ], + showFileSize: true, + showFileLastModified: true, + }); + await expect(wrapper.getElement()).toValidateA11y(); + }); + + test('multiple w/ warnings', async () => { + const wrapper = render({ + items: [ + { file: file1, warningText: 'Warning 1' }, + { file: file2, warningText: 'Warning 2' }, + ], + showFileSize: true, + showFileLastModified: true, + }); + await expect(wrapper.getElement()).toValidateA11y(); + }); +}); diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 08b537479f..224de1e75a 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -17,6 +17,8 @@ import tokenListStyles from '../token-list/styles.css.js'; import styles from './styles.css.js'; import testStyles from './test-classes/styles.css.js'; +export { FileTokenGroupProps }; + type InternalFileTokenGroupProps = FileTokenGroupProps & InternalBaseComponentProps; function InternalFileTokenGroup({ diff --git a/src/test-utils/dom/file-upload/index.ts b/src/test-utils/dom/file-upload/index.ts index 836d6fd883..2ed919d8dd 100644 --- a/src/test-utils/dom/file-upload/index.ts +++ b/src/test-utils/dom/file-upload/index.ts @@ -3,8 +3,8 @@ import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; import ButtonWrapper from '../button'; +import { FileTokenWrapper } from '../internal/file-token-group'; -import fileUploadOptionSelectors from '../../../file-upload/file-option/styles.selectors.js'; import fileUploadSelectors from '../../../file-upload/styles.selectors.js'; import formFieldStyles from '../../../form-field/styles.selectors.js'; import fileUploadInputSelectors from '../../../internal/components/file-input/styles.selectors.js'; @@ -56,33 +56,3 @@ export default class FileUploadWrapper extends ComponentWrapper { return this.find(`.${fileUploadSelectors.hints} .${formFieldStyles.warning} .${formFieldStyles.warning__message}`); } } - -class FileTokenWrapper extends ComponentWrapper { - findFileName(): ElementWrapper { - return this.findByClassName(fileUploadOptionSelectors['file-option-name'])!; - } - - findFileSize(): null | ElementWrapper { - return this.findByClassName(fileUploadOptionSelectors['file-option-size']); - } - - findFileLastModified(): null | ElementWrapper { - return this.findByClassName(fileUploadOptionSelectors['file-option-last-modified']); - } - - findFileThumbnail(): null | ElementWrapper { - return this.findByClassName(fileUploadOptionSelectors['file-option-thumbnail-image']); - } - - findFileError(): null | ElementWrapper { - return this.find(`.${formFieldStyles.error} .${formFieldStyles.error__message}`); - } - - findFileWarning(): null | ElementWrapper { - return this.find(`.${formFieldStyles.warning} .${formFieldStyles.warning__message}`); - } - - findRemoveButton(): ElementWrapper { - return this.findByClassName(tokenGroupSelectors['dismiss-button'])!; - } -} diff --git a/src/test-utils/dom/internal/file-token-group.ts b/src/test-utils/dom/internal/file-token-group.ts new file mode 100644 index 0000000000..feda224769 --- /dev/null +++ b/src/test-utils/dom/internal/file-token-group.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; + +import formFieldStyles from '../../../form-field/styles.selectors.js'; +import selectors from '../../../internal/components/file-token-group/styles.selectors.js'; +import testSelectors from '../../../internal/components/file-token-group/test-classes/styles.selectors.js'; +import tokenGroupSelectors from '../../../token-group/styles.selectors.js'; + +export default class FileTokenGroupWrapper extends ComponentWrapper { + static rootSelector: string = testSelectors.root; + + findFileTokens(): Array { + return this.findAllByClassName(tokenGroupSelectors.token).map( + tokenElement => new FileTokenWrapper(tokenElement.getElement()) + ); + } + + /** + * Returns a file token from for a given index. + * + * @param tokenIndex 1-based index of the file token to return. + */ + findFileToken(fileTokenIndex: number): null | FileTokenWrapper { + return this.findComponent(`.${tokenGroupSelectors.token}[data-index="${fileTokenIndex - 1}"]`, FileTokenWrapper); + } +} + +export class FileTokenWrapper extends ComponentWrapper { + static rootSelector: string = selectors['file-token']; + + findFileName(): ElementWrapper { + return this.findByClassName(selectors['file-option-name'])!; + } + + findFileSize(): ElementWrapper { + return this.findByClassName(selectors['file-option-size'])!; + } + + findFileLastModified(): ElementWrapper { + return this.findByClassName(selectors['file-option-last-modified'])!; + } + + findFileThumbnail(): ElementWrapper { + return this.findByClassName(selectors['file-option-thumbnail'])!; + } + + findFileError(): ElementWrapper { + return this.find(`.${formFieldStyles.error} .${formFieldStyles.error__message}`)!; + } + + findFileWarning(): ElementWrapper { + return this.find(`.${formFieldStyles.warning} .${formFieldStyles.warning__message}`)!; + } + + findRemoveButton(): ElementWrapper { + return this.findByClassName(tokenGroupSelectors['dismiss-button'])!; + } +} From 26f839d659960d4201b0946bd48e8a4772ed72c8 Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 28 Oct 2024 18:38:38 -0700 Subject: [PATCH 04/24] fix: Fix loading, tests, and general cleanup --- .../__tests__/file-token-group.test.tsx | 9 ++------- .../components/file-token-group/file-token.tsx | 16 ++++++++-------- .../components/file-token-group/index.tsx | 2 +- .../components/file-token-group/styles.scss | 13 +++++++++---- src/token-group/styles.scss | 8 ++++++-- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx index 2046d8a0cb..ea182f1b75 100644 --- a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -180,6 +180,7 @@ describe('File upload tokens', () => { { file: file2, errorText: 'Error 2' }, ], }); + expect(wrapper.findFileToken(1)!.findFileError().getElement()).toHaveTextContent('Error 1'); expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Error 1'); expect(wrapper.findFileToken(2)!.getElement()).toHaveAccessibleDescription('Error 2'); }); @@ -191,6 +192,7 @@ describe('File upload tokens', () => { { file: file2, warningText: 'Warning 2' }, ], }); + expect(wrapper.findFileToken(1)!.findFileWarning().getElement()).toHaveTextContent('Warning 1'); expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Warning 1'); expect(wrapper.findFileToken(2)!.getElement()).toHaveAccessibleDescription('Warning 2'); }); @@ -218,13 +220,6 @@ describe('Focusing behavior', () => { expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus(); }); - - // test('Focus is dispatched to the file input when the last token is removed, multiple=%s', () => { - // const wrapper = renderStateful({ items: [{ file: file1 }] }); - // wrapper.findFileToken(1)!.findRemoveButton().click(); - - // expect(wrapper.findNativeInput().getElement()).toHaveFocus(); - // }); }); describe('a11y', () => { diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index a87e243d14..97b992f2b6 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -77,14 +77,6 @@ function InternalFileToken({ onFocus={() => setShowTooltip(true)} onBlur={() => setShowTooltip(false)} > - {loading && ( - <> -
      -
      - -
      - - )} + {loading && ( + <> +
      +
      + +
      + + )} {showFileThumbnail && isImage && } diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 224de1e75a..98d9939517 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -71,7 +71,7 @@ function InternalFileTokenGroup({ warningText={file.warningText} i18nStrings={i18nStrings} disabled={file.disabled} - loading={file.loading} + loading={true} loadingText={file.loadingText} alignment={alignment} groupContainsImage={groupContainsImage} diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss index 6099dc36ca..65464eee6e 100644 --- a/src/internal/components/file-token-group/styles.scss +++ b/src/internal/components/file-token-group/styles.scss @@ -7,6 +7,7 @@ @use '../../styles' as styles; $image-size: 48px; +$icon-offset: awsui.$size-icon-normal / 2; .root { @include styles.styles-reset(); @@ -19,15 +20,19 @@ $image-size: 48px; .file-loading-overlay { position: absolute; - inline-size: 100%; - block-size: 100%; + inset: 0; background: awsui.$color-background-container-content; opacity: 75%; + border-start-start-radius: awsui.$border-radius-token; + border-start-end-radius: awsui.$border-radius-token; + border-end-start-radius: awsui.$border-radius-token; + border-end-end-radius: awsui.$border-radius-token; + &-spinner { position: absolute; - inset-inline-start: calc(50% - 8px); - inset-block-end: calc(50% - 8px); + inset-inline-start: calc(50% - $icon-offset); + inset-block-end: calc(50% - $icon-offset); } } diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss index 8b8ed7c9e0..d05b714f34 100644 --- a/src/token-group/styles.scss +++ b/src/token-group/styles.scss @@ -8,6 +8,9 @@ @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; @use './constants' as constants; +$file-token-height: 70px; +$compact-token-width: 220px; + .root { @include styles.styles-reset; @@ -50,11 +53,12 @@ gap: awsui.$space-xxs; &-contains-image { - grid-template-rows: 70px auto; + grid-template-rows: $file-token-height auto; } } .token-box { + position: relative; block-size: 100%; border-block: awsui.$border-field-width solid constants.$token-border-color; border-inline: awsui.$border-field-width solid constants.$token-border-color; @@ -73,7 +77,7 @@ box-sizing: border-box; &.horizontal { - max-inline-size: 220px; + max-inline-size: $compact-token-width; } } @mixin token-box-validation { From 8aa79d051780dbe162642ebff0b5fa54343e36f9 Mon Sep 17 00:00:00 2001 From: Katie George Date: Wed, 30 Oct 2024 13:40:40 -0700 Subject: [PATCH 05/24] fix: Fixes loading spinner --- .../components/file-token-group/file-token.tsx | 8 ++++---- .../components/file-token-group/index.tsx | 2 +- .../components/file-token-group/styles.scss | 17 ++--------------- src/token-group/styles.scss | 5 +++++ src/token-group/token.tsx | 3 +++ 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index 97b992f2b6..df667d6858 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -86,14 +86,14 @@ function InternalFileToken({ errorIconAriaLabel={i18nStrings.errorIconAriaLabel} warningIconAriaLabel={i18nStrings.warningIconAriaLabel} disabled={disabled} + loading={loading} alignment={alignment} groupContainsImage={groupContainsImage && showFileThumbnail && alignment === 'horizontal' && !imageError} data-index={index} > {loading && ( <> -
      -
      +
      @@ -119,7 +119,7 @@ function InternalFileToken({ {showFileSize && file.size ? ( {formatFileSize(file.size)} @@ -129,7 +129,7 @@ function InternalFileToken({ {showFileLastModified && file.lastModified ? ( {formatFileLastModified(new Date(file.lastModified))} diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 98d9939517..224de1e75a 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -71,7 +71,7 @@ function InternalFileTokenGroup({ warningText={file.warningText} i18nStrings={i18nStrings} disabled={file.disabled} - loading={true} + loading={file.loading} loadingText={file.loadingText} alignment={alignment} groupContainsImage={groupContainsImage} diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss index 65464eee6e..7eaf63519b 100644 --- a/src/internal/components/file-token-group/styles.scss +++ b/src/internal/components/file-token-group/styles.scss @@ -7,7 +7,6 @@ @use '../../styles' as styles; $image-size: 48px; -$icon-offset: awsui.$size-icon-normal / 2; .root { @include styles.styles-reset(); @@ -20,20 +19,8 @@ $icon-offset: awsui.$size-icon-normal / 2; .file-loading-overlay { position: absolute; - inset: 0; - background: awsui.$color-background-container-content; - opacity: 75%; - - border-start-start-radius: awsui.$border-radius-token; - border-start-end-radius: awsui.$border-radius-token; - border-end-start-radius: awsui.$border-radius-token; - border-end-end-radius: awsui.$border-radius-token; - - &-spinner { - position: absolute; - inset-inline-start: calc(50% - $icon-offset); - inset-block-end: calc(50% - $icon-offset); - } + inset-inline-end: awsui.$space-static-xs; + inset-block-end: awsui.$space-static-xxs; } .file-option-name, diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss index d05b714f34..791595deb1 100644 --- a/src/token-group/styles.scss +++ b/src/token-group/styles.scss @@ -128,3 +128,8 @@ $compact-token-width: 220px; } } } + +.token-box-loading { + border-color: awsui.$color-border-control-disabled; + background-color: awsui.$color-background-container-content; +} diff --git a/src/token-group/token.tsx b/src/token-group/token.tsx index 2c4d62abe9..bca45976f4 100644 --- a/src/token-group/token.tsx +++ b/src/token-group/token.tsx @@ -18,6 +18,7 @@ interface TokenProps { dismissLabel?: string; onDismiss?: () => void; disabled?: boolean; + loading?: boolean; readOnly?: boolean; errorText?: React.ReactNode; errorIconAriaLabel?: string; @@ -31,6 +32,7 @@ interface TokenProps { export function Token({ ariaLabel, disabled, + loading, readOnly, dismissLabel, onDismiss, @@ -64,6 +66,7 @@ export function Token({ className={clsx( styles['token-box'], disabled && styles['token-box-disabled'], + loading && styles['token-box-loading'], readOnly && styles['token-box-readonly'], errorText && styles['token-box-error'], showWarning && styles['token-box-warning'], From 1c3d007a9771002e62aee62fe7f2d94dc3b5abcd Mon Sep 17 00:00:00 2001 From: Katie George Date: Wed, 30 Oct 2024 14:20:25 -0700 Subject: [PATCH 06/24] fix: Show tooltip on when necessary --- .../file-token-group/file-token.tsx | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index df667d6858..c5664d730d 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import clsx from 'clsx'; import InternalBox from '../../../box/internal.js'; @@ -66,17 +66,24 @@ function InternalFileToken({ const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; - const containerRef = React.useRef(null); + const containerRef = useRef(null); + const fileNameRef = useRef(null); + const fileNameContainerRef = useRef(null); const [showTooltip, setShowTooltip] = useState(false); const [imageError, setImageError] = useState(false); + function isEllipsisActive() { + const span = fileNameRef.current; + const container = fileNameContainerRef.current; + + if (span && container) { + return span.offsetWidth >= container.offsetWidth; + } + return false; + } + return ( -
      setShowTooltip(true)} - onBlur={() => setShowTooltip(false)} - > +
      -
      setShowTooltip(true)} onMouseOut={() => setShowTooltip(false)}> +
      setShowTooltip(true)} + onMouseOut={() => setShowTooltip(false)} + ref={fileNameContainerRef} + > - {file.name} + {file.name}
      @@ -139,7 +150,7 @@ function InternalFileToken({
      - {alignment === 'horizontal' && showTooltip && ( + {alignment === 'horizontal' && showTooltip && isEllipsisActive() && ( Date: Wed, 30 Oct 2024 14:32:10 -0700 Subject: [PATCH 07/24] fix: Update token height and width --- pages/file-upload/scenario-standalone.page.tsx | 7 ++++++- src/internal/components/token-list/styles.scss | 2 +- src/token-group/styles.scss | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pages/file-upload/scenario-standalone.page.tsx b/pages/file-upload/scenario-standalone.page.tsx index bd2dc7a562..7e1abe54f1 100644 --- a/pages/file-upload/scenario-standalone.page.tsx +++ b/pages/file-upload/scenario-standalone.page.tsx @@ -12,6 +12,7 @@ import { validateContractFiles } from './validations'; export default function FileUploadScenarioStandalone() { const contractsRef = useRef(null); const [acceptMultiple, setAcceptMultiple] = useState(true); + const [verticalAlignment, setVerticalAlignment] = useState(true); const formState = useContractFilesForm(); const contractsValidationErrors = validateContractFiles(formState.files); @@ -40,12 +41,16 @@ export default function FileUploadScenarioStandalone() { Accept multiple files + setVerticalAlignment(event.detail.checked)}> + Vertical alignment + + Date: Wed, 30 Oct 2024 14:36:03 -0700 Subject: [PATCH 08/24] chore: Removes disabled variant --- src/internal/components/file-token-group/file-token.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index c5664d730d..68b427d7bf 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -39,7 +39,6 @@ export interface FileTokenProps extends BaseComponentProps { loading?: boolean; loadingText?: string; i18nStrings: FileTokenProps.I18nStrings; - disabled?: boolean; dismissLabel?: string; alignment?: TokenGroupProps.Alignment; groupContainsImage?: boolean; @@ -55,7 +54,6 @@ function InternalFileToken({ onDismiss, errorText, warningText, - disabled, loading, loadingText, alignment, @@ -92,7 +90,6 @@ function InternalFileToken({ warningText={warningText} errorIconAriaLabel={i18nStrings.errorIconAriaLabel} warningIconAriaLabel={i18nStrings.warningIconAriaLabel} - disabled={disabled} loading={loading} alignment={alignment} groupContainsImage={groupContainsImage && showFileThumbnail && alignment === 'horizontal' && !imageError} From ffd0b20343a85fedb0ee730617eeccb4dec0b7de Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 31 Oct 2024 15:24:05 -0700 Subject: [PATCH 09/24] fix: Simplify i18nStrings --- src/file-upload/internal.tsx | 22 ++----------------- .../components/file-token-group/index.tsx | 1 - 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index b438941f0e..a0d699b3ed 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -14,7 +14,6 @@ import { getBaseProps } from '../internal/base-component'; import InternalFileDropzone, { useFilesDragging } from '../internal/components/file-dropzone'; import InternalFileInput from '../internal/components/file-input'; import InternalFileTokenGroup from '../internal/components/file-token-group'; -import * as defaultFormatters from '../internal/components/file-token-group/default-formatters'; import InternalFileToken from '../internal/components/file-token-group/file-token'; import { fireNonCancelableEvent } from '../internal/events'; import checkControlled from '../internal/hooks/check-controlled'; @@ -79,9 +78,6 @@ function InternalFileUpload( const fileInputRef = useRef(null); const ref = useMergeRefs(fileInputRef, externalRef); - const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; - const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; - checkControlled('FileUpload', 'value', value, 'onChange', onChange); if (!multiple && value.length > 1) { @@ -177,13 +173,7 @@ function InternalFileUpload( errorText={fileErrors?.[0]} warningText={fileWarnings?.[0]} onDismiss={() => onFileRemove(0)} - i18nStrings={{ - removeFileAriaLabel: () => i18nStrings.removeFileAriaLabel(0), - errorIconAriaLabel: i18nStrings.errorIconAriaLabel, - warningIconAriaLabel: i18nStrings.warningIconAriaLabel, - formatFileLastModified: date => formatFileLastModified(date), - formatFileSize: size => formatFileSize(size), - }} + i18nStrings={i18nStrings} index={0} /> ) : null} @@ -200,15 +190,7 @@ function InternalFileUpload( showFileLastModified={metadata.showFileLastModified} showFileSize={metadata.showFileSize} showFileThumbnail={metadata.showFileThumbnail} - i18nStrings={{ - removeFileAriaLabel: index => i18nStrings.removeFileAriaLabel(index), - errorIconAriaLabel: i18nStrings.errorIconAriaLabel, - warningIconAriaLabel: i18nStrings.warningIconAriaLabel, - formatFileLastModified: date => formatFileLastModified(date), - formatFileSize: size => formatFileSize(size), - limitShowMore: i18nStrings.limitShowMore, - limitShowFewer: i18nStrings.limitShowFewer, - }} + i18nStrings={i18nStrings} onDismiss={event => onFileRemove(event.detail.fileIndex)} /> ) : null} diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 224de1e75a..83ab3eb5ce 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -70,7 +70,6 @@ function InternalFileTokenGroup({ errorText={file.errorText} warningText={file.warningText} i18nStrings={i18nStrings} - disabled={file.disabled} loading={file.loading} loadingText={file.loadingText} alignment={alignment} From 90bef0aa77f12e8792d4f8f0eb08e86fd6ac000b Mon Sep 17 00:00:00 2001 From: Katie George Date: Fri, 1 Nov 2024 11:49:48 -0700 Subject: [PATCH 10/24] fix: Responsive behavior and constants --- src/internal/components/token-list/styles.scss | 8 +++++++- src/token-group/constants.scss | 4 +++- src/token-group/styles.scss | 11 ++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/internal/components/token-list/styles.scss b/src/internal/components/token-list/styles.scss index 3aad00920c..f32df30c00 100644 --- a/src/internal/components/token-list/styles.scss +++ b/src/internal/components/token-list/styles.scss @@ -5,6 +5,7 @@ @use '../../styles/tokens' as awsui; @use '../../styles' as styles; +@use '../../../token-group/constants' as constants; @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; .root { @@ -45,7 +46,12 @@ &.grid { display: grid; gap: awsui.$space-xs; - grid-template-columns: repeat(auto-fill, 230px); + grid-template-columns: repeat(auto-fill, constants.$compact-token-width); + + @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { + display: flex; + flex-direction: column; + } } } diff --git a/src/token-group/constants.scss b/src/token-group/constants.scss index 9785c3eaab..e23695f090 100644 --- a/src/token-group/constants.scss +++ b/src/token-group/constants.scss @@ -7,5 +7,7 @@ @use '../internal/styles/tokens' as awsui; $token-background: awsui.$color-background-item-selected; - $token-border-color: awsui.$color-border-item-selected; + +$file-token-height: 68.5px; +$compact-token-width: 230px; diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss index 1dcbfa7bae..758dd7291c 100644 --- a/src/token-group/styles.scss +++ b/src/token-group/styles.scss @@ -8,9 +8,6 @@ @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; @use './constants' as constants; -$file-token-height: 68.5px; -$compact-token-width: 230px; - .root { @include styles.styles-reset; @@ -53,7 +50,7 @@ $compact-token-width: 230px; gap: awsui.$space-xxs; &-contains-image { - grid-template-rows: $file-token-height auto; + grid-template-rows: constants.$file-token-height auto; } } @@ -77,7 +74,11 @@ $compact-token-width: 230px; box-sizing: border-box; &.horizontal { - max-inline-size: $compact-token-width; + max-inline-size: constants.$compact-token-width; + + @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { + max-inline-size: 100%; + } } } @mixin token-box-validation { From 18af63aed0c493dfa3a1a7538afa2fc70cd1c837 Mon Sep 17 00:00:00 2001 From: Katie George Date: Fri, 1 Nov 2024 13:43:50 -0700 Subject: [PATCH 11/24] fix: Fixes fallback text and updates demo pages --- pages/file-upload/permutations.page.tsx | 3 ++- .../components/file-token-group/file-token.tsx | 5 ++--- .../components/file-token-group/styles.scss | 13 +++++++++++++ .../components/file-token-group/thumbnail.tsx | 13 ++----------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pages/file-upload/permutations.page.tsx b/pages/file-upload/permutations.page.tsx index c5cbec1239..cb0348755a 100644 --- a/pages/file-upload/permutations.page.tsx +++ b/pages/file-upload/permutations.page.tsx @@ -26,7 +26,7 @@ const permutations = createPermutations(null); const fileNameContainerRef = useRef(null); const [showTooltip, setShowTooltip] = useState(false); - const [imageError, setImageError] = useState(false); function isEllipsisActive() { const span = fileNameRef.current; @@ -92,7 +91,7 @@ function InternalFileToken({ warningIconAriaLabel={i18nStrings.warningIconAriaLabel} loading={loading} alignment={alignment} - groupContainsImage={groupContainsImage && showFileThumbnail && alignment === 'horizontal' && !imageError} + groupContainsImage={groupContainsImage && showFileThumbnail && alignment === 'horizontal'} data-index={index} > {loading && ( @@ -103,7 +102,7 @@ function InternalFileToken({ )} - {showFileThumbnail && isImage && } + {showFileThumbnail && isImage && }
      void; } -export function FileOptionThumbnail({ file, setHasError }: FileOptionThumbnailProps) { +export function FileOptionThumbnail({ file }: FileOptionThumbnailProps) { const [imageSrc, setImageSrc] = useState(''); useEffect(() => { @@ -27,15 +26,7 @@ export function FileOptionThumbnail({ file, setHasError }: FileOptionThumbnailPr return (
      - {file.name} { - setHasError(true); - currentTarget.onerror = null; // prevents looping - }} - /> + {file.name}
      ); } From 6f5354a22e41e323a462e125caba5c98c934bb4a Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 4 Nov 2024 08:47:04 -0800 Subject: [PATCH 12/24] chore: Updates test utils --- .../__snapshots__/test-utils-selectors.test.tsx.snap | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index ea13fb73e6..273fdea14c 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -250,10 +250,6 @@ exports[`test-utils selectors 1`] = ` "awsui_root_gwq0h", ], "file-upload": [ - "awsui_file-option-last-modified_ezgb4", - "awsui_file-option-name_ezgb4", - "awsui_file-option-size_ezgb4", - "awsui_file-option-thumbnail-image_ezgb4", "awsui_hints_1ubbm", "awsui_root_1ubbm", ], @@ -339,6 +335,11 @@ exports[`test-utils selectors 1`] = ` "awsui_dropdown_qwoo0", "awsui_file-input-button_181f9", "awsui_file-input_181f9", + "awsui_file-option-last-modified_ofwwb", + "awsui_file-option-name_ofwwb", + "awsui_file-option-size_ofwwb", + "awsui_file-option-thumbnail_ofwwb", + "awsui_file-token_ofwwb", "awsui_filter-container_z5mul", "awsui_filtering-match-highlight_1p2cx", "awsui_handle_sdha6", @@ -365,6 +366,7 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1qprf", "awsui_root_1t44z", "awsui_root_1tk3k", + "awsui_root_9f1dn", "awsui_root_qwoo0", "awsui_root_vrgzu", "awsui_selectable-item_15o6u", From 16b4c205122db6788924d8428637d61e0f4b798b Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 4 Nov 2024 11:18:26 -0800 Subject: [PATCH 13/24] chore: Adds tests to cover ellipsis and tooltip --- .../__tests__/file-token-group.test.tsx | 50 ++++++++++++++++++- .../file-token-group/file-token.tsx | 4 +- .../file-token-group/test-classes/styles.scss | 3 +- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx index ea182f1b75..e994e934e7 100644 --- a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; -import { render as testingLibraryRender } from '@testing-library/react'; +import { act, fireEvent, render as testingLibraryRender } from '@testing-library/react'; import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; @@ -12,6 +12,9 @@ import FileTokenGroup, { import createWrapper from '../../../../../lib/components/test-utils/dom'; import FileTokenGroupWrapper from '../../../../../lib/components/test-utils/dom/internal/file-token-group'; +import styles from '../../../../../lib/components/internal/components/file-token-group/test-classes/styles.css.js'; +import tooltipStyles from '../../../../../lib/components/internal/components/tooltip/styles.selectors.js'; + jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), warnOnce: jest.fn(), @@ -48,6 +51,14 @@ const file2 = new File([new Blob(['Test content 2'], { type: 'text/plain' })], ' type: 'image/png', lastModified: 1590962400000, }); +const file3 = new File( + [new Blob(['Test content 3'], { type: 'text/plain' })], + 'test-file-3-with-a-really-long-name.txt', + { + type: 'image/png', + lastModified: 1590962400000, + } +); function render(props: Partial) { testingLibraryRender( @@ -206,6 +217,43 @@ describe('File upload tokens', () => { }); }); +describe('Tooltip', () => { + test('Should show ellipsis on long file names', () => { + const wrapper = render({ items: [{ file: file3 }] }); + act(() => { + fireEvent.mouseEnter(wrapper.findFileToken(1)!.findFileName().getElement()); + }); + + expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveClass(styles['ellipsis-active']); + }); + + test('Should show tooltip on mouse enter', () => { + const wrapper = render({ items: [{ file: file3 }], alignment: 'horizontal' }); + + act(() => { + fireEvent.mouseEnter(wrapper.findFileToken(1)!.findFileName().getElement()); + }); + + expect(document.querySelector(`.${tooltipStyles.root}`)).not.toBeNull(); + + act(() => { + fireEvent.mouseLeave(wrapper.findFileToken(1)!.findFileName().getElement()); + }); + + expect(document.querySelector(`.${tooltipStyles.root}`)).toBeNull(); + }); + + test('Should not show tooltip with vertical alignment', () => { + const wrapper = render({ items: [{ file: file3 }] }); + + act(() => { + fireEvent.mouseEnter(wrapper.findFileToken(1)!.findFileName().getElement()); + }); + + expect(document.querySelector(`.${tooltipStyles.root}`)).toBeNull(); + }); +}); + describe('Focusing behavior', () => { test.each([1, 2])(`Focus is dispatched to the next token when the token before it is removed, limit=%s`, limit => { const wrapper = renderStateful({ items: [{ file: file1 }, { file: file2 }], limit }); diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index fe82be3f38..d4c333378e 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -117,7 +117,9 @@ function InternalFileToken({ > {file.name} diff --git a/src/internal/components/file-token-group/test-classes/styles.scss b/src/internal/components/file-token-group/test-classes/styles.scss index eae0803276..634f0265e6 100644 --- a/src/internal/components/file-token-group/test-classes/styles.scss +++ b/src/internal/components/file-token-group/test-classes/styles.scss @@ -6,6 +6,7 @@ .file-option-thumbnail, .file-option-name, .file-option-size, -.file-option-last-modified { +.file-option-last-modified, +.ellipsis-active { /* used in test-utils */ } From 65784571fd1cd9cd58160a537d0a7d0cb11237d2 Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 4 Nov 2024 11:21:22 -0800 Subject: [PATCH 14/24] chore: updates documenter --- .../__snapshots__/documenter.test.ts.snap | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 82330a588c..96f2adecb4 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -7453,6 +7453,20 @@ is provided by its parent form field component. "optional": true, "type": "ReadonlyArray", }, + { + "description": "Alignment of the file tokens. Defaults to "vertical".", + "inlineType": { + "name": "FileUploadProps.FileTokenAlignment", + "type": "union", + "values": [ + "vertical", + "horizontal", + ], + }, + "name": "fileTokenAlignment", + "optional": true, + "type": "string", + }, { "description": "An array of file warnings corresponding to the files in the \`value\`.", "name": "fileWarnings", From a35bfae637e500801d0ce9b9057c5482488128ea Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 4 Nov 2024 11:35:15 -0800 Subject: [PATCH 15/24] chore: file height tweak --- src/token-group/constants.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/token-group/constants.scss b/src/token-group/constants.scss index e23695f090..77f9fd30f2 100644 --- a/src/token-group/constants.scss +++ b/src/token-group/constants.scss @@ -9,5 +9,5 @@ $token-background: awsui.$color-background-item-selected; $token-border-color: awsui.$color-border-item-selected; -$file-token-height: 68.5px; +$file-token-height: 68px; $compact-token-width: 230px; From c7cdd968f0900d4954f097f95a6babf95e951879 Mon Sep 17 00:00:00 2001 From: Katie George Date: Mon, 4 Nov 2024 11:54:19 -0800 Subject: [PATCH 16/24] fix: Adds file type to token --- src/internal/components/file-token-group/file-token.tsx | 1 + src/token-group/styles.scss | 9 +++++++-- src/token-group/token.tsx | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index d4c333378e..94a83ad0d5 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -82,6 +82,7 @@ function InternalFileToken({ return (
      Date: Thu, 7 Nov 2024 13:41:26 -0800 Subject: [PATCH 17/24] chore: Removes unnecessary tests and renders list for all --- .../__tests__/file-upload.test.tsx | 118 ------------------ src/file-upload/internal.tsx | 17 +-- .../__tests__/file-token-group.test.tsx | 4 +- 3 files changed, 3 insertions(+), 136 deletions(-) diff --git a/src/file-upload/__tests__/file-upload.test.tsx b/src/file-upload/__tests__/file-upload.test.tsx index 8ac785ee43..8bcea46d70 100644 --- a/src/file-upload/__tests__/file-upload.test.tsx +++ b/src/file-upload/__tests__/file-upload.test.tsx @@ -10,8 +10,6 @@ import FileUpload, { FileUploadProps } from '../../../lib/components/file-upload import createWrapper from '../../../lib/components/test-utils/dom'; import FileDropzoneWrapper from '../../../lib/components/test-utils/dom/internal/file-dropzone'; -import tokenListSelectors from '../../../lib/components/internal/components/token-list/styles.selectors.js'; - jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), warnOnce: jest.fn(), @@ -214,11 +212,6 @@ describe('FileUpload input', () => { }); describe('File upload tokens', () => { - test('token list is not rendered when `multiple=false`', () => { - const wrapper = render({ multiple: false, value: [file1] }); - expect(wrapper.find(tokenListSelectors.root)).toBeNull(); - }); - test(`when multiple=true all file tokens are shown`, () => { const wrapper = render({ multiple: true, value: [file1, file2] }); @@ -231,100 +224,6 @@ describe('File upload tokens', () => { expect(wrapper.findFileToken(2)!.getElement()).toHaveTextContent('test-file-2.txt'); }); - test('dev warning is issued when using multiple files with a singular file upload', () => { - render({ value: [file1, file2] }); - - expect(warnOnce).toHaveBeenCalledTimes(1); - expect(warnOnce).toHaveBeenCalledWith('FileUpload', 'Value must be an array of size 0 or 1 when `multiple=false`.'); - }); - - test('file token remove button has ARIA label set', () => { - const wrapper = render({ value: [file1] }); - expect(wrapper.findFileToken(1)!.findRemoveButton()!.getElement()).toHaveAccessibleName('Remove file 1'); - }); - - test('selected file can be removed', () => { - const wrapperSingular = render({ value: [file1] }); - wrapperSingular.findFileToken(1)!.findRemoveButton()!.click(); - - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [] } })); - - const wrapperMultiple = render({ multiple: true, value: [file1, file2] }); - wrapperMultiple.findFileToken(1)!.findRemoveButton()!.click(); - - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [file2] } })); - }); - - test('file token only shows name by default', () => { - const wrapper = render({ value: [file1] }); - - expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt'); - expect(wrapper.findFileToken(1)!.findFileSize()).toBe(null); - expect(wrapper.findFileToken(1)!.findFileLastModified()).toBe(null); - expect(wrapper.findFileToken(1)!.findFileThumbnail()).toBe(null); - }); - - test('file token metadata can be opt-in', () => { - const wrapper = render({ - value: [file1], - showFileSize: true, - showFileLastModified: true, - }); - expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveTextContent('test-file-1.txt'); - expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('0.01 KB'); - expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020-06-01T00:00:00'); - }); - - test('thumbnail is only shown when file type starts with "image"', () => { - expect( - render({ value: [file1], showFileThumbnail: true }) - .findFileToken(1)! - .findFileThumbnail() - ).toBe(null); - - expect( - render({ value: [file2], showFileThumbnail: true }) - .findFileToken(1)! - .findFileThumbnail() - ).not.toBe(null); - }); - - test('selected file size can be customized', () => { - const wrapper = render({ - value: [file1], - showFileSize: true, - i18nStrings: { - ...defaultProps.i18nStrings, - formatFileSize: sizeInBytes => `${sizeInBytes} bytes`, - }, - }); - expect(wrapper.findFileToken(1)!.findFileSize()!.getElement()).toHaveTextContent('14 bytes'); - }); - - test('selected file last update timestamp can be customized', () => { - const wrapper = render({ - value: [file1], - showFileLastModified: true, - i18nStrings: { - ...defaultProps.i18nStrings, - formatFileLastModified: date => `${date.getFullYear()} year`, - }, - }); - expect(wrapper.findFileToken(1)!.findFileLastModified()!.getElement()).toHaveTextContent('2020 year'); - }); - - test('the `tokenLimit` property controls the number of tokens shown by default', () => { - const wrapper = render({ multiple: true, value: [file1, file2], tokenLimit: 1 }); - expect(wrapper.findFileTokens()).toHaveLength(1); - expect(wrapper.getElement().textContent).toContain('Show more files'); - }); - - test('file tokens have aria labels set to file names', () => { - const wrapper = render({ multiple: true, value: [file1, file2] }); - expect(wrapper.findFileToken(1)!.getElement()).toHaveAttribute('aria-label', file1.name); - expect(wrapper.findFileToken(2)!.getElement()).toHaveAttribute('aria-label', file2.name); - }); - test('file errors are associated to file tokens', () => { const wrapper = render({ multiple: true, value: [file1, file2], fileErrors: ['Error 1', 'Error 2'] }); expect(wrapper.findFileToken(1)!.getElement()).toHaveAccessibleDescription('Error 1'); @@ -358,23 +257,6 @@ describe('File upload dropzone', () => { }); describe('Focusing behavior', () => { - test.each([1, 2])( - `Focus is dispatched to the next token when the token before it is removed, tokenLimit=%s`, - tokenLimit => { - const wrapper = renderStateful({ multiple: true, value: [file1, file2], tokenLimit }); - wrapper.findFileToken(1)!.findRemoveButton().click(); - - expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus(); - } - ); - - test('Focus is dispatched to the previous token when removing the token at the end', () => { - const wrapper = renderStateful({ multiple: true, value: [file1, file2] }); - wrapper.findFileToken(2)!.findRemoveButton().click(); - - expect(wrapper.findFileToken(1)!.findRemoveButton().getElement()).toHaveFocus(); - }); - test.each([false, true])( 'Focus is dispatched to the file input when the last token is removed, multiple=%s', multiple => { diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index a0d699b3ed..56b0e79f9d 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -14,7 +14,6 @@ import { getBaseProps } from '../internal/base-component'; import InternalFileDropzone, { useFilesDragging } from '../internal/components/file-dropzone'; import InternalFileInput from '../internal/components/file-input'; import InternalFileTokenGroup from '../internal/components/file-token-group'; -import InternalFileToken from '../internal/components/file-token-group/file-token'; import { fireNonCancelableEvent } from '../internal/events'; import checkControlled from '../internal/hooks/check-controlled'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; @@ -164,21 +163,7 @@ function InternalFileUpload( )} - {!multiple && value.length > 0 ? ( - onFileRemove(0)} - i18nStrings={i18nStrings} - index={0} - /> - ) : null} - - {multiple && value.length > 0 ? ( + {value.length > 0 ? ( { }); describe('a11y', () => { - test('multiple empty', async () => { + test('empty', async () => { const wrapper = render({ items: [] }); await expect(wrapper.getElement()).toValidateA11y(); }); @@ -305,7 +305,7 @@ describe('a11y', () => { await expect(wrapper.getElement()).toValidateA11y(); }); - test('multiple w/o errors nor warnings', async () => { + test('multiple', async () => { const wrapper = render({ items: [{ file: file1 }, { file: file2 }], showFileSize: true, From 3a161a2f8827013141f136e4aa0ebbf9d3eef8b5 Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 7 Nov 2024 14:14:21 -0800 Subject: [PATCH 18/24] fix: Fixes single row loading bug and removes loadingText --- src/file-upload/internal.tsx | 1 + .../__tests__/file-token-group.test.tsx | 21 ++++++++----------- .../file-token-group/file-token.tsx | 20 ++++++++++-------- .../components/file-token-group/index.tsx | 1 - .../components/file-token-group/interfaces.ts | 4 +--- .../components/file-token-group/styles.scss | 9 ++++++++ 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index 56b0e79f9d..afb3b23818 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -169,6 +169,7 @@ function InternalFileUpload( alignment={fileTokenAlignment} items={value.map((file, fileIndex) => ({ file, + loading: true, errorText: fileErrors?.[fileIndex], warningText: fileWarnings?.[fileIndex], }))} diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx index 7aeabcb90f..5783b662f9 100644 --- a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -3,8 +3,6 @@ import React, { useState } from 'react'; import { act, fireEvent, render as testingLibraryRender } from '@testing-library/react'; -import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; - import '../../../../__a11y__/to-validate-a11y'; import FileTokenGroup, { FileTokenGroupProps, @@ -14,11 +12,7 @@ import FileTokenGroupWrapper from '../../../../../lib/components/test-utils/dom/ import styles from '../../../../../lib/components/internal/components/file-token-group/test-classes/styles.css.js'; import tooltipStyles from '../../../../../lib/components/internal/components/tooltip/styles.selectors.js'; - -jest.mock('@cloudscape-design/component-toolkit/internal', () => ({ - ...jest.requireActual('@cloudscape-design/component-toolkit/internal'), - warnOnce: jest.fn(), -})); +import spinnerStyles from '../../../../../lib/components/spinner/styles.selectors.js'; jest.mock('../../../../../lib/components/internal/utils/date-time', () => ({ formatDateTime: () => '2020-06-01T00:00:00', @@ -26,11 +20,6 @@ jest.mock('../../../../../lib/components/internal/utils/date-time', () => ({ const onDismiss = jest.fn(); -afterEach(() => { - (warnOnce as jest.Mock).mockReset(); - onDismiss.mockReset(); -}); - const defaultProps: FileTokenGroupProps = { items: [], onDismiss, @@ -217,6 +206,14 @@ describe('File upload tokens', () => { }); }); +describe('File loading', () => { + test('Spinner added when loading', () => { + render({ items: [{ file: file1, loading: true }] }); + + expect(document.querySelector(`.${spinnerStyles.root}`)).not.toBeNull(); + }); +}); + describe('Tooltip', () => { test('Should show ellipsis on long file names', () => { const wrapper = render({ items: [{ file: file3 }] }); diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index 94a83ad0d5..2145fd9a2c 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -5,7 +5,6 @@ import React, { useRef, useState } from 'react'; import clsx from 'clsx'; import InternalBox from '../../../box/internal.js'; -import InternalLiveRegion from '../../../live-region/internal'; import InternalSpaceBetween from '../../../space-between/internal.js'; import InternalSpinner from '../../../spinner/internal.js'; import { TokenGroupProps } from '../../../token-group/interfaces.js'; @@ -37,7 +36,6 @@ export interface FileTokenProps extends BaseComponentProps { errorText?: React.ReactNode; warningText?: React.ReactNode; loading?: boolean; - loadingText?: string; i18nStrings: FileTokenProps.I18nStrings; dismissLabel?: string; alignment?: TokenGroupProps.Alignment; @@ -55,7 +53,6 @@ function InternalFileToken({ errorText, warningText, loading, - loadingText, alignment, groupContainsImage, index, @@ -79,6 +76,9 @@ function InternalFileToken({ return false; } + const fileIsSingleRow = + !showFileLastModified && !showFileSize && (!groupContainsImage || (groupContainsImage && !showFileThumbnail)); + return (
      {loading && ( - <> -
      - -
      - +
      + +
      )} {showFileThumbnail && isImage && } @@ -108,6 +110,7 @@ function InternalFileToken({
      @@ -156,7 +159,6 @@ function InternalFileToken({ value={{file.name}} /> )} - {loading && loadingText && {loadingText}}
      ); } diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 83ab3eb5ce..484eb5f5d3 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -71,7 +71,6 @@ function InternalFileTokenGroup({ warningText={file.warningText} i18nStrings={i18nStrings} loading={file.loading} - loadingText={file.loadingText} alignment={alignment} groupContainsImage={groupContainsImage} index={fileIndex} diff --git a/src/internal/components/file-token-group/interfaces.ts b/src/internal/components/file-token-group/interfaces.ts index fcdad2a21d..d5685f9144 100644 --- a/src/internal/components/file-token-group/interfaces.ts +++ b/src/internal/components/file-token-group/interfaces.ts @@ -39,8 +39,7 @@ export interface FileTokenGroupProps extends BaseComponentProps { * * - `file` (string) - File value. * - `disabled` [boolean] - (Optional) Determines whether the token is disabled. - * - `loading` (boolean) - (Optional) Custom SVG icon. Equivalent to the `svg` slot of the [icon component](/components/icon/). - * - `loadingText` (string) - (Optional) Specifies the text that screen reader announces when the button is in a loading state. + * - `loading` (boolean) - (Optional) Determine whether the token is loading. * - `errorText` (string) - (Optional) Text that displays as a validation error message. * - `warningText` (string) - (Optional) - Text that displays as a validation warning message. */ @@ -93,7 +92,6 @@ export namespace FileTokenGroupProps { file: File; disabled?: boolean; loading?: boolean; - loadingText?: string; errorText?: null | string; warningText?: null | string; } diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss index bf92821637..dfb090f121 100644 --- a/src/internal/components/file-token-group/styles.scss +++ b/src/internal/components/file-token-group/styles.scss @@ -7,6 +7,7 @@ @use '../../styles' as styles; $image-size: 48px; +$spinner-size: 16px; .root { @include styles.styles-reset(); @@ -21,6 +22,10 @@ $image-size: 48px; position: absolute; inset-inline-end: awsui.$space-static-xs; inset-block-end: awsui.$space-static-xxs; + + &-single-row { + inset-inline-end: awsui.$space-static-xxl; + } } .file-option-name, @@ -67,4 +72,8 @@ $image-size: 48px; &.with-image { inline-size: calc(100% - $image-size); } + + &.single-row-loading { + inline-size: calc(100% - $spinner-size); + } } From 8d2194d45c40b0a02ba00f7e00d733ea69e2be0b Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 7 Nov 2024 14:26:29 -0800 Subject: [PATCH 19/24] fix: isImage and horizontal grid alignment --- src/file-upload/internal.tsx | 1 - src/internal/components/file-token-group/file-token.tsx | 3 ++- src/internal/components/file-token-group/index.tsx | 4 ++-- src/internal/components/token-list/index.tsx | 5 ++--- src/internal/components/token-list/interfaces.ts | 3 +-- src/token-group/dismiss-button.tsx | 6 ++---- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index afb3b23818..56b0e79f9d 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -169,7 +169,6 @@ function InternalFileUpload( alignment={fileTokenAlignment} items={value.map((file, fileIndex) => ({ file, - loading: true, errorText: fileErrors?.[fileIndex], warningText: fileWarnings?.[fileIndex], }))} diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index 2145fd9a2c..8a55c8c4e9 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -40,6 +40,7 @@ export interface FileTokenProps extends BaseComponentProps { dismissLabel?: string; alignment?: TokenGroupProps.Alignment; groupContainsImage?: boolean; + isImage: boolean; index: number; } @@ -55,9 +56,9 @@ function InternalFileToken({ loading, alignment, groupContainsImage, + isImage, index, }: FileTokenProps) { - const isImage = file.type.startsWith('image/'); const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; diff --git a/src/internal/components/file-token-group/index.tsx b/src/internal/components/file-token-group/index.tsx index 484eb5f5d3..cf008e261d 100644 --- a/src/internal/components/file-token-group/index.tsx +++ b/src/internal/components/file-token-group/index.tsx @@ -54,8 +54,7 @@ function InternalFileTokenGroup({ return (
      ( )} diff --git a/src/internal/components/token-list/index.tsx b/src/internal/components/token-list/index.tsx index c92f8791e7..68a1ba05ff 100644 --- a/src/internal/components/token-list/index.tsx +++ b/src/internal/components/token-list/index.tsx @@ -21,7 +21,6 @@ export default function TokenList({ i18nStrings, limitShowFewerAriaLabel, limitShowMoreAriaLabel, - isGrid = false, onExpandedClick = () => undefined, }: TokenListProps) { const controlId = useUniqueId(); @@ -82,8 +81,8 @@ export default function TokenList({ id={controlId} className={clsx(styles.list, { [styles.vertical]: alignment === 'vertical', - [styles.horizontal]: alignment === 'horizontal' && !isGrid, - [styles.grid]: alignment === 'horizontal' && isGrid, + [styles.horizontal]: alignment === 'horizontal', + [styles.grid]: alignment === 'horizontal-grid', })} > {visibleItems.map((item, itemIndex) => ( diff --git a/src/internal/components/token-list/interfaces.ts b/src/internal/components/token-list/interfaces.ts index a053949727..d66fbb9bf9 100644 --- a/src/internal/components/token-list/interfaces.ts +++ b/src/internal/components/token-list/interfaces.ts @@ -4,7 +4,7 @@ import React from 'react'; export interface TokenListProps { - alignment: 'vertical' | 'horizontal' | 'inline'; + alignment: 'vertical' | 'horizontal' | 'inline' | 'horizontal-grid'; items: readonly Item[]; limit?: number; after?: React.ReactNode; @@ -13,7 +13,6 @@ export interface TokenListProps { onExpandedClick?: (isExpanded: boolean) => void; limitShowFewerAriaLabel?: string; limitShowMoreAriaLabel?: string; - isGrid?: boolean; } export interface I18nStrings { diff --git a/src/token-group/dismiss-button.tsx b/src/token-group/dismiss-button.tsx index 72c74a8762..94761a5a8a 100644 --- a/src/token-group/dismiss-button.tsx +++ b/src/token-group/dismiss-button.tsx @@ -5,7 +5,6 @@ import React, { forwardRef, Ref } from 'react'; import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; import InternalIcon from '../icon/internal'; -import InternalSpinner from '../spinner/internal'; import { GeneratedAnalyticsMetadataTokenGroupDismiss } from './analytics-metadata/interfaces'; import styles from './styles.css.js'; @@ -13,7 +12,6 @@ import styles from './styles.css.js'; interface DismissButtonProps { disabled?: boolean; readOnly?: boolean; - loading?: boolean; onDismiss?: () => void; dismissLabel?: string; } @@ -21,7 +19,7 @@ interface DismissButtonProps { export default forwardRef(DismissButton); function DismissButton( - { disabled, dismissLabel, onDismiss, readOnly, loading }: DismissButtonProps, + { disabled, dismissLabel, onDismiss, readOnly }: DismissButtonProps, ref: Ref ) { const analyticsMetadata: GeneratedAnalyticsMetadataTokenGroupDismiss = { @@ -45,7 +43,7 @@ function DismissButton( aria-label={dismissLabel} {...(disabled || readOnly ? {} : getAnalyticsMetadataAttribute(analyticsMetadata))} > - {loading ? : } + ); } From 92a851038f180adbc939d70228ad2f06e72ac7f5 Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 7 Nov 2024 15:12:28 -0800 Subject: [PATCH 20/24] feat: Separates file token from token --- .../file-token-group/file-token.tsx | 57 +++++++++---- .../components/file-token-group/index.tsx | 2 + .../components/file-token-group/styles.scss | 83 +++++++++++++++++-- .../components/token-list/styles.scss | 2 +- src/test-utils/dom/file-upload/index.ts | 2 +- .../dom/internal/file-token-group.ts | 6 +- src/token-group/constants.scss | 3 - src/token-group/mixins.scss | 27 ++++++ src/token-group/styles.scss | 58 +------------ src/token-group/token.tsx | 55 +----------- 10 files changed, 158 insertions(+), 137 deletions(-) create mode 100644 src/token-group/mixins.scss diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index 8a55c8c4e9..546a80e0fd 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -5,11 +5,13 @@ import React, { useRef, useState } from 'react'; import clsx from 'clsx'; import InternalBox from '../../../box/internal.js'; +import { FormFieldError, FormFieldWarning } from '../../../form-field/internal'; import InternalSpaceBetween from '../../../space-between/internal.js'; import InternalSpinner from '../../../spinner/internal.js'; +import DismissButton from '../../../token-group/dismiss-button'; import { TokenGroupProps } from '../../../token-group/interfaces.js'; -import { Token } from '../../../token-group/token.js'; import { BaseComponentProps } from '../../base-component/index.js'; +import { useUniqueId } from '../../hooks/use-unique-id'; import Tooltip from '../tooltip/index'; import * as defaultFormatters from './default-formatters.js'; import { FileOptionThumbnail } from './thumbnail.js'; @@ -36,6 +38,7 @@ export interface FileTokenProps extends BaseComponentProps { errorText?: React.ReactNode; warningText?: React.ReactNode; loading?: boolean; + readOnly?: boolean; i18nStrings: FileTokenProps.I18nStrings; dismissLabel?: string; alignment?: TokenGroupProps.Alignment; @@ -53,6 +56,7 @@ function InternalFileToken({ onDismiss, errorText, warningText, + readOnly, loading, alignment, groupContainsImage, @@ -62,6 +66,10 @@ function InternalFileToken({ const formatFileSize = i18nStrings.formatFileSize ?? defaultFormatters.formatFileSize; const formatFileLastModified = i18nStrings.formatFileLastModified ?? defaultFormatters.formatFileLastModified; + const errorId = useUniqueId('error'); + const warningId = useUniqueId('warning'); + + const showWarning = warningText && !errorText; const containerRef = useRef(null); const fileNameRef = useRef(null); const fileNameContainerRef = useRef(null); @@ -81,20 +89,24 @@ function InternalFileToken({ !showFileLastModified && !showFileSize && (!groupContainsImage || (groupContainsImage && !showFileThumbnail)); return ( -
      - +
      {loading && (
      - + {onDismiss && !readOnly && ( + + )} +
      + {errorText && ( + + {errorText} + + )} + {showWarning && ( + + {warningText} + + )} {alignment === 'horizontal' && showTooltip && isEllipsisActive() && ( .dismiss-button { + color: awsui.$color-text-interactive-default; + &:hover { + color: awsui.$color-text-interactive-hover; + } + } } -.file-token { - block-size: 100%; - position: relative; +.root { + @include styles.styles-reset(); } .file-loading-overlay { @@ -77,3 +90,63 @@ $spinner-size: 16px; inline-size: calc(100% - $spinner-size); } } + +.token { + position: relative; + block-size: 100%; + display: flex; + flex-direction: column; + gap: awsui.$space-xxs; + + &-grid { + display: grid; + grid-template-rows: max-content auto; + } + + &-contains-image { + grid-template-rows: $file-token-height auto; + } +} + +.token-box { + @include mixins.token-box-styles(); + + &.horizontal { + max-inline-size: $compact-token-width; + + @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { + max-inline-size: 100%; + } + } + + &.error { + border-color: awsui.$color-border-status-error; + @include token-box-validation; + } + + &.warning { + border-color: awsui.$color-border-status-warning; + @include token-box-validation; + } + + &.read-only { + border-color: awsui.$color-border-input-disabled; + background-color: awsui.$color-background-container-content; + pointer-events: none; + + > .dismiss-button { + color: awsui.$color-text-button-inline-icon-disabled; + + &:hover { + /* stylelint-disable-next-line plugin/no-unsupported-browser-features */ + cursor: initial; + color: awsui.$color-text-button-inline-icon-disabled; + } + } + } + + &.loading { + border-color: awsui.$color-border-control-disabled; + background-color: awsui.$color-background-container-content; + } +} diff --git a/src/internal/components/token-list/styles.scss b/src/internal/components/token-list/styles.scss index f32df30c00..5edc11bc06 100644 --- a/src/internal/components/token-list/styles.scss +++ b/src/internal/components/token-list/styles.scss @@ -46,7 +46,7 @@ &.grid { display: grid; gap: awsui.$space-xs; - grid-template-columns: repeat(auto-fill, constants.$compact-token-width); + grid-template-columns: repeat(auto-fill, 230px); @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { display: flex; diff --git a/src/test-utils/dom/file-upload/index.ts b/src/test-utils/dom/file-upload/index.ts index 2ed919d8dd..a14cd54c9a 100644 --- a/src/test-utils/dom/file-upload/index.ts +++ b/src/test-utils/dom/file-upload/index.ts @@ -8,8 +8,8 @@ import { FileTokenWrapper } from '../internal/file-token-group'; import fileUploadSelectors from '../../../file-upload/styles.selectors.js'; import formFieldStyles from '../../../form-field/styles.selectors.js'; import fileUploadInputSelectors from '../../../internal/components/file-input/styles.selectors.js'; +import tokenGroupSelectors from '../../../internal/components/file-token-group/styles.selectors.js'; import tokenListSelectors from '../../../internal/components/token-list/styles.selectors.js'; -import tokenGroupSelectors from '../../../token-group/styles.selectors.js'; export default class FileUploadWrapper extends ComponentWrapper { static rootSelector: string = fileUploadSelectors.root; diff --git a/src/test-utils/dom/internal/file-token-group.ts b/src/test-utils/dom/internal/file-token-group.ts index feda224769..33cd7526ac 100644 --- a/src/test-utils/dom/internal/file-token-group.ts +++ b/src/test-utils/dom/internal/file-token-group.ts @@ -11,7 +11,7 @@ export default class FileTokenGroupWrapper extends ComponentWrapper { static rootSelector: string = testSelectors.root; findFileTokens(): Array { - return this.findAllByClassName(tokenGroupSelectors.token).map( + return this.findAllByClassName(selectors.token).map( tokenElement => new FileTokenWrapper(tokenElement.getElement()) ); } @@ -22,12 +22,12 @@ export default class FileTokenGroupWrapper extends ComponentWrapper { * @param tokenIndex 1-based index of the file token to return. */ findFileToken(fileTokenIndex: number): null | FileTokenWrapper { - return this.findComponent(`.${tokenGroupSelectors.token}[data-index="${fileTokenIndex - 1}"]`, FileTokenWrapper); + return this.findComponent(`.${selectors.token}[data-index="${fileTokenIndex - 1}"]`, FileTokenWrapper); } } export class FileTokenWrapper extends ComponentWrapper { - static rootSelector: string = selectors['file-token']; + static rootSelector: string = selectors.token; findFileName(): ElementWrapper { return this.findByClassName(selectors['file-option-name'])!; diff --git a/src/token-group/constants.scss b/src/token-group/constants.scss index 77f9fd30f2..6ed92a762d 100644 --- a/src/token-group/constants.scss +++ b/src/token-group/constants.scss @@ -8,6 +8,3 @@ $token-background: awsui.$color-background-item-selected; $token-border-color: awsui.$color-border-item-selected; - -$file-token-height: 68px; -$compact-token-width: 230px; diff --git a/src/token-group/mixins.scss b/src/token-group/mixins.scss new file mode 100644 index 0000000000..8c08f0bf09 --- /dev/null +++ b/src/token-group/mixins.scss @@ -0,0 +1,27 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ +@use '../internal/styles' as styles; +@use '../internal/styles/tokens' as awsui; +@use './constants' as constants; + +@mixin token-box-styles { + position: relative; + block-size: 100%; + border-block: awsui.$border-field-width solid constants.$token-border-color; + border-inline: awsui.$border-field-width solid constants.$token-border-color; + padding-block-start: styles.$control-padding-vertical; + padding-block-end: styles.$control-padding-vertical; + padding-inline-start: styles.$control-padding-horizontal; + padding-inline-end: awsui.$space-xxs; + display: flex; + align-items: flex-start; + background: constants.$token-background; + border-start-start-radius: awsui.$border-radius-token; + border-start-end-radius: awsui.$border-radius-token; + border-end-start-radius: awsui.$border-radius-token; + border-end-end-radius: awsui.$border-radius-token; + color: awsui.$color-text-body-default; + box-sizing: border-box; +} diff --git a/src/token-group/styles.scss b/src/token-group/styles.scss index f56efac250..b8c85efd43 100644 --- a/src/token-group/styles.scss +++ b/src/token-group/styles.scss @@ -7,6 +7,7 @@ @use '../internal/styles/tokens' as awsui; @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; @use './constants' as constants; +@use './mixins.scss' as mixins; .root { @include styles.styles-reset; @@ -48,62 +49,12 @@ display: flex; flex-direction: column; gap: awsui.$space-xxs; - - &-grid { - display: grid; - grid-template-rows: max-content auto; - } - - &-contains-image { - grid-template-rows: constants.$file-token-height auto; - } } .token-box { - position: relative; - block-size: 100%; - border-block: awsui.$border-field-width solid constants.$token-border-color; - border-inline: awsui.$border-field-width solid constants.$token-border-color; - padding-block-start: styles.$control-padding-vertical; - padding-block-end: styles.$control-padding-vertical; - padding-inline-start: styles.$control-padding-horizontal; - padding-inline-end: awsui.$space-xxs; - display: flex; - align-items: flex-start; - background: constants.$token-background; - border-start-start-radius: awsui.$border-radius-token; - border-start-end-radius: awsui.$border-radius-token; - border-end-start-radius: awsui.$border-radius-token; - border-end-end-radius: awsui.$border-radius-token; - color: awsui.$color-text-body-default; - box-sizing: border-box; - - &.horizontal { - max-inline-size: constants.$compact-token-width; - - @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { - max-inline-size: 100%; - } - } + @include mixins.token-box-styles(); } -@mixin token-box-validation { - border-inline-start-width: awsui.$border-invalid-width; - > .dismiss-button { - color: awsui.$color-text-interactive-default; - &:hover { - color: awsui.$color-text-interactive-hover; - } - } -} -.token-box-error { - border-color: awsui.$color-border-status-error; - @include token-box-validation; -} -.token-box-warning { - border-color: awsui.$color-border-status-warning; - @include token-box-validation; -} .token-box-readonly { border-color: awsui.$color-border-input-disabled; background-color: awsui.$color-background-container-content; @@ -134,8 +85,3 @@ } } } - -.token-box-loading { - border-color: awsui.$color-border-control-disabled; - background-color: awsui.$color-background-container-content; -} diff --git a/src/token-group/token.tsx b/src/token-group/token.tsx index 9d25672fec..6d741da18f 100644 --- a/src/token-group/token.tsx +++ b/src/token-group/token.tsx @@ -4,11 +4,8 @@ import React from 'react'; import clsx from 'clsx'; -import { FormFieldError, FormFieldWarning } from '../form-field/internal'; import { getBaseProps } from '../internal/base-component'; -import { useUniqueId } from '../internal/hooks/use-unique-id'; import DismissButton from './dismiss-button'; -import { TokenGroupProps } from './interfaces'; import styles from './styles.css.js'; @@ -18,62 +15,26 @@ interface TokenProps { dismissLabel?: string; onDismiss?: () => void; disabled?: boolean; - loading?: boolean; readOnly?: boolean; - errorText?: React.ReactNode; - errorIconAriaLabel?: string; - warningText?: React.ReactNode; - warningIconAriaLabel?: string; - alignment?: TokenGroupProps.Alignment; - groupContainsImage?: boolean; - type?: 'default' | 'file'; className?: string; } -export function Token({ - ariaLabel, - disabled, - loading, - readOnly, - dismissLabel, - onDismiss, - children, - errorText, - warningText, - errorIconAriaLabel, - warningIconAriaLabel, - alignment, - groupContainsImage, - type = 'default', - ...restProps -}: TokenProps) { - const errorId = useUniqueId('error'); - const warningId = useUniqueId('warning'); +export function Token({ ariaLabel, disabled, readOnly, dismissLabel, onDismiss, children, ...restProps }: TokenProps) { const baseProps = getBaseProps(restProps); - const showWarning = warningText && !errorText; - return (
      {children} @@ -81,16 +42,6 @@ export function Token({ )}
      - {errorText && ( - - {errorText} - - )} - {showWarning && ( - - {warningText} - - )}
      ); } From 1b9c2328acb248abec03f7aa92c705d02829ce46 Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 7 Nov 2024 15:48:41 -0800 Subject: [PATCH 21/24] fix: Fix failing tests --- .../test-utils-selectors.test.tsx.snap | 2 +- src/token-group/__tests__/token-group.test.tsx | 17 +---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 273fdea14c..c8025d20ff 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -339,7 +339,6 @@ exports[`test-utils selectors 1`] = ` "awsui_file-option-name_ofwwb", "awsui_file-option-size_ofwwb", "awsui_file-option-thumbnail_ofwwb", - "awsui_file-token_ofwwb", "awsui_filter-container_z5mul", "awsui_filtering-match-highlight_1p2cx", "awsui_handle_sdha6", @@ -376,6 +375,7 @@ exports[`test-utils selectors 1`] = ` "awsui_ticks--y_f0fot", "awsui_title_1kjc7", "awsui_toggle_gfwv3", + "awsui_token_ofwwb", "awsui_value_10ipo", "awsui_wrapper_1wepg", ], diff --git a/src/token-group/__tests__/token-group.test.tsx b/src/token-group/__tests__/token-group.test.tsx index cf94372e4a..5f6a5fe21d 100644 --- a/src/token-group/__tests__/token-group.test.tsx +++ b/src/token-group/__tests__/token-group.test.tsx @@ -1,12 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React, { useState } from 'react'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import TestI18nProvider from '../../../lib/components/i18n/testing'; import createWrapper, { IconWrapper, TokenGroupWrapper } from '../../../lib/components/test-utils/dom'; import TokenGroup, { TokenGroupProps } from '../../../lib/components/token-group'; -import { Token } from '../../../lib/components/token-group/token'; import { getIconHTML } from '../../icon/__tests__/utils'; import optionSelectors from '../../../lib/components/internal/components/option/styles.selectors.js'; @@ -315,20 +314,6 @@ describe('TokenGroup', () => { }); }); -describe('Token', () => { - test('Renders token error and associates it with the token', () => { - const { container } = render( - - Content - - ); - const tokenElement = createWrapper(container).findByClassName(selectors.token)!.getElement(); - expect(screen.getByLabelText('Error icon')).toBeDefined(); - expect(screen.getByText('Error text')).toBeDefined(); - expect(tokenElement).toHaveAccessibleDescription('Error text'); - }); -}); - describe('i18n', () => { test('supports rendering limitShowFewer and limitShowMore using i18n provider', () => { const { container } = render( From 295ce33dfa88976824dcc023280f5a4847e4617d Mon Sep 17 00:00:00 2001 From: Katie George Date: Thu, 7 Nov 2024 15:59:49 -0800 Subject: [PATCH 22/24] chore: Moves constants into separate file --- .../file-token-group/constants.scss | 14 +++++++++++++ .../components/file-token-group/styles.scss | 21 +++++++------------ .../components/token-list/styles.scss | 4 ++-- src/token-group/dismiss-button.tsx | 1 + 4 files changed, 24 insertions(+), 16 deletions(-) create mode 100644 src/internal/components/file-token-group/constants.scss diff --git a/src/internal/components/file-token-group/constants.scss b/src/internal/components/file-token-group/constants.scss new file mode 100644 index 0000000000..6c9aba203f --- /dev/null +++ b/src/internal/components/file-token-group/constants.scss @@ -0,0 +1,14 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '../../styles/tokens' as awsui; + +$image-size: 48px; +$file-token-height: 68px; +$compact-token-width: 230px; +$spinner-size: awsui.$size-icon-normal; + +$token-background: awsui.$color-background-item-selected; +$token-border-color: awsui.$color-border-item-selected; diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss index beb6f5e6b9..5304324278 100644 --- a/src/internal/components/file-token-group/styles.scss +++ b/src/internal/components/file-token-group/styles.scss @@ -5,17 +5,10 @@ @use '../../styles/tokens' as awsui; @use '../../styles' as styles; +@use './constants' as constants; @use '../../../token-group/mixins.scss' as mixins; @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; -$image-size: 48px; -$spinner-size: 16px; -$file-token-height: 68px; -$compact-token-width: 230px; - -$token-background: awsui.$color-background-item-selected; -$token-border-color: awsui.$color-border-item-selected; - @mixin token-box-validation { border-inline-start-width: awsui.$border-invalid-width; @@ -63,8 +56,8 @@ $token-border-color: awsui.$color-border-item-selected; .file-option-thumbnail-image { @include styles.font-body-s; - inline-size: $image-size; - block-size: $image-size; + inline-size: constants.$image-size; + block-size: constants.$image-size; object-fit: cover; display: -webkit-box; @@ -83,11 +76,11 @@ $token-border-color: awsui.$color-border-item-selected; inline-size: 100%; &.with-image { - inline-size: calc(100% - $image-size); + inline-size: calc(100% - constants.$image-size); } &.single-row-loading { - inline-size: calc(100% - $spinner-size); + inline-size: calc(100% - constants.$spinner-size); } } @@ -104,7 +97,7 @@ $token-border-color: awsui.$color-border-item-selected; } &-contains-image { - grid-template-rows: $file-token-height auto; + grid-template-rows: constants.$file-token-height auto; } } @@ -112,7 +105,7 @@ $token-border-color: awsui.$color-border-item-selected; @include mixins.token-box-styles(); &.horizontal { - max-inline-size: $compact-token-width; + max-inline-size: constants.$compact-token-width; @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { max-inline-size: 100%; diff --git a/src/internal/components/token-list/styles.scss b/src/internal/components/token-list/styles.scss index 5edc11bc06..b7eb6bd882 100644 --- a/src/internal/components/token-list/styles.scss +++ b/src/internal/components/token-list/styles.scss @@ -5,7 +5,7 @@ @use '../../styles/tokens' as awsui; @use '../../styles' as styles; -@use '../../../token-group/constants' as constants; +@use '../../../internal/components/file-token-group/constants' as constants; @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; .root { @@ -46,7 +46,7 @@ &.grid { display: grid; gap: awsui.$space-xs; - grid-template-columns: repeat(auto-fill, 230px); + grid-template-columns: repeat(auto-fill, constants.$compact-token-width); @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { display: flex; diff --git a/src/token-group/dismiss-button.tsx b/src/token-group/dismiss-button.tsx index 94761a5a8a..91037853f6 100644 --- a/src/token-group/dismiss-button.tsx +++ b/src/token-group/dismiss-button.tsx @@ -38,6 +38,7 @@ function DismissButton( if (disabled || readOnly || !onDismiss) { return; } + onDismiss(); }} aria-label={dismissLabel} From a7a2acdc9795ec8415c83e5c2829afaad06b4a28 Mon Sep 17 00:00:00 2001 From: Katie George Date: Fri, 8 Nov 2024 10:36:50 -0800 Subject: [PATCH 23/24] fix: Adds tooltip to vertical alignment, and updates tests --- .../__tests__/file-token-group.test.tsx | 35 +++++++++++-------- .../file-token-group/file-token.tsx | 6 ++-- .../components/file-token-group/interfaces.ts | 2 -- .../components/file-token-group/styles.scss | 8 +---- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx index 5783b662f9..14fca2e7e8 100644 --- a/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx +++ b/src/internal/components/file-token-group/__tests__/file-token-group.test.tsx @@ -10,9 +10,9 @@ import FileTokenGroup, { import createWrapper from '../../../../../lib/components/test-utils/dom'; import FileTokenGroupWrapper from '../../../../../lib/components/test-utils/dom/internal/file-token-group'; -import styles from '../../../../../lib/components/internal/components/file-token-group/test-classes/styles.css.js'; +import styles from '../../../../../lib/components/internal/components/file-token-group/styles.css.js'; +import testStyles from '../../../../../lib/components/internal/components/file-token-group/test-classes/styles.css.js'; import tooltipStyles from '../../../../../lib/components/internal/components/tooltip/styles.selectors.js'; -import spinnerStyles from '../../../../../lib/components/spinner/styles.selectors.js'; jest.mock('../../../../../lib/components/internal/utils/date-time', () => ({ formatDateTime: () => '2020-06-01T00:00:00', @@ -207,10 +207,18 @@ describe('File upload tokens', () => { }); describe('File loading', () => { + test('Aria-disabled added when loading', () => { + const wrapper = render({ items: [{ file: file1, loading: true }, { file: file2 }] }); + + expect(wrapper.findFileToken(1)?.getElement()).toHaveAttribute('aria-disabled'); + expect(wrapper.findFileToken(2)?.getElement()).not.toHaveAttribute('aria-disabled'); + }); + test('Spinner added when loading', () => { - render({ items: [{ file: file1, loading: true }] }); + const wrapper = render({ items: [{ file: file1, loading: true }, { file: file2 }] }); - expect(document.querySelector(`.${spinnerStyles.root}`)).not.toBeNull(); + expect(wrapper.findFileToken(1)?.getElement().firstChild).toHaveClass(styles.loading); + expect(wrapper.findFileToken(2)?.getElement().firstChild).not.toHaveClass(styles.loading); }); }); @@ -221,7 +229,7 @@ describe('Tooltip', () => { fireEvent.mouseEnter(wrapper.findFileToken(1)!.findFileName().getElement()); }); - expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveClass(styles['ellipsis-active']); + expect(wrapper.findFileToken(1)!.findFileName().getElement()).toHaveClass(testStyles['ellipsis-active']); }); test('Should show tooltip on mouse enter', () => { @@ -239,16 +247,6 @@ describe('Tooltip', () => { expect(document.querySelector(`.${tooltipStyles.root}`)).toBeNull(); }); - - test('Should not show tooltip with vertical alignment', () => { - const wrapper = render({ items: [{ file: file3 }] }); - - act(() => { - fireEvent.mouseEnter(wrapper.findFileToken(1)!.findFileName().getElement()); - }); - - expect(document.querySelector(`.${tooltipStyles.root}`)).toBeNull(); - }); }); describe('Focusing behavior', () => { @@ -334,4 +332,11 @@ describe('a11y', () => { }); await expect(wrapper.getElement()).toValidateA11y(); }); + + test('loading', async () => { + const wrapper = render({ + items: [{ file: file1, loading: true }], + }); + await expect(wrapper.getElement()).toValidateA11y(); + }); }); diff --git a/src/internal/components/file-token-group/file-token.tsx b/src/internal/components/file-token-group/file-token.tsx index 546a80e0fd..c3e852777c 100644 --- a/src/internal/components/file-token-group/file-token.tsx +++ b/src/internal/components/file-token-group/file-token.tsx @@ -91,12 +91,14 @@ function InternalFileToken({ return (
      )} - {alignment === 'horizontal' && showTooltip && isEllipsisActive() && ( + {showTooltip && isEllipsisActive() && ( Date: Fri, 8 Nov 2024 10:41:00 -0800 Subject: [PATCH 24/24] fix: Fixes responsive behavior --- src/internal/components/file-token-group/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/internal/components/file-token-group/styles.scss b/src/internal/components/file-token-group/styles.scss index aeff435f97..0992cdef79 100644 --- a/src/internal/components/file-token-group/styles.scss +++ b/src/internal/components/file-token-group/styles.scss @@ -88,6 +88,10 @@ &-grid { display: grid; grid-template-rows: max-content auto; + + @include styles.media-breakpoint-down(styles.$breakpoint-x-small) { + display: flex; + } } &-contains-image {