diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d430c6d33bbbf2..bb9d1c0d0e0175 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -331,6 +331,8 @@ packages/react-components/component-selector-preview/library @microsoft/teams-pr packages/react-components/component-selector-preview/stories @microsoft/teams-prg packages/react-components/react-menu-grid-preview/library @microsoft/teams-prg packages/react-components/react-menu-grid-preview/stories @microsoft/teams-prg +packages/react-components/react-file-type-icons/library @microsoft/cxe-red @jahnp @bigbadcapers +packages/react-components/react-file-type-icons/stories @microsoft/cxe-red @jahnp @bigbadcapers # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/README.md b/README.md index f57e18143be43e..b59baadfe6d708 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ [![Build Status](https://img.shields.io/azure-devops/build/uifabric/fabricpublic/164/master?style=flat-square)](https://dev.azure.com/uifabric/fabricpublic/_build?definitionId=164) ![GitHub contributors](https://img.shields.io/github/contributors/microsoft/fluentui?style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/microsoft/fluentui?style=flat-square) [![Twitter Follow](https://img.shields.io/twitter/follow/fluentui?logo=x&style=flat-square)](https://twitter.com/FluentUI?ref_src=twsrc%5Etfw) -> Fluent UI React is shipping its v9 final stable release. Visit the [Fluent UI React v9 Release page on the wiki](https://github.com/microsoft/fluentui/wiki/Fluent-UI-React-v9-Release) to learn more about the upcoming release schedule. - -Fluent UI web represents a collection of utilities, React components, and Web Components for building web applications. +Fluent UI is a collection of utilities and React components for building web applications. This repo is home to 3 separate projects today. Combining Fluent UI React v9 components with Fluent UI React v8 or v0 components is possible and allows gradual migration to Fluent UI v9. @@ -27,6 +25,8 @@ The following table will help you navigate the 3 projects and understand their d > Why are there two React versions? Fluent UI v8 is still widely used. We encourage you to migrate to Fluent UI v9. See the [Migration overview](https://react.fluentui.dev/?path=/docs/concepts-migration-from-v8-component-mapping--docs). +For docs on how to contribute to our repo, check out the [contributing docs](https://github.com/microsoft/fluentui/tree/master/docs/react-v9/contributing) folder. + ## FluentUI Insights [Fluent UI Insights](https://docs.microsoft.com/en-us/shows/fluent-ui-insights?utm_source=github) is a series that describes the design and decisions behind the Fluent UI design system. diff --git a/apps/pr-deploy-site/chiclet-test.html b/apps/pr-deploy-site/chiclet-test.html index 8d4c379d0202ad..32f79c65016e8e 100644 --- a/apps/pr-deploy-site/chiclet-test.html +++ b/apps/pr-deploy-site/chiclet-test.html @@ -16,7 +16,7 @@ /> Chiclet Test Page diff --git a/apps/public-docsite/package.json b/apps/public-docsite/package.json index 84a013c8e8fac3..776e69559c7e75 100644 --- a/apps/public-docsite/package.json +++ b/apps/public-docsite/package.json @@ -39,7 +39,7 @@ "@fluentui/react-examples": "*", "@fluentui/react-experiments": "*", "@fluentui/fluent2-theme": "*", - "@fluentui/react-file-type-icons": "*", + "@fluentui/react-file-type-icons-v8": "*", "@fluentui/react-icons-mdl2": "*", "@fluentui/react-icons-mdl2-branded": "*", "@fluentui/set-version": "*", diff --git a/apps/public-docsite/src/pages/Styles/FileTypeIconsPage/FileTypeIconsPage.tsx b/apps/public-docsite/src/pages/Styles/FileTypeIconsPage/FileTypeIconsPage.tsx index 0f0218460a2919..bbf62b5774880b 100644 --- a/apps/public-docsite/src/pages/Styles/FileTypeIconsPage/FileTypeIconsPage.tsx +++ b/apps/public-docsite/src/pages/Styles/FileTypeIconsPage/FileTypeIconsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Icon, Link } from '@fluentui/react'; -import { getFileTypeIconProps } from '@fluentui/react-file-type-icons'; +import { getFileTypeIconProps } from '@fluentui/react-file-type-icons-v8'; import { Markdown, MarkdownHeader, IPageSectionProps } from '@fluentui/react-docsite-components/lib/index2'; import { IStylesPageProps, StylesAreaPage } from '../StylesAreaPage'; import { FileTypeIconsPageProps } from './FileTypeIconsPage.doc'; diff --git a/apps/public-docsite/src/root.tsx b/apps/public-docsite/src/root.tsx index 8cd4d118a27760..4373ad99366451 100644 --- a/apps/public-docsite/src/root.tsx +++ b/apps/public-docsite/src/root.tsx @@ -1,5 +1,5 @@ import { registerIcons, on, KeyCodes, setRTL } from '@fluentui/react'; -import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons'; +import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons-v8'; import { createSite } from './utilities/createSite'; import * as platformPickerStyles from '@fluentui/react-docsite-components/lib/components/PlatformPicker/PlatformPicker.module.scss'; import { SiteDefinition } from './SiteDefinition/index'; diff --git a/apps/vr-tests/src/stories/Tile.stories.tsx b/apps/vr-tests/src/stories/Tile.stories.tsx index b0fb8a9652e943..eedce33d890865 100644 --- a/apps/vr-tests/src/stories/Tile.stories.tsx +++ b/apps/vr-tests/src/stories/Tile.stories.tsx @@ -142,7 +142,7 @@ export const DocumentTileWithIcon = () => ( itemActivity={}>{'Test Activity'}} foreground={ Document; +``` + +**v9 (component-based) - Recommended:** + +```tsx +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; + +; +``` + +## Migration Examples + +### Document Icons + +**Before:** + +```tsx +import { getFileTypeIconProps } from '@fluentui/react-file-type-icons'; + +function DocumentItem({ filename }) { + const extension = filename.split('.').pop(); + const iconProps = getFileTypeIconProps({ extension, size: 32 }); + + return ( +
+ {filename} + {filename} +
+ ); +} +``` + +**After:** + +```tsx +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; + +function DocumentItem({ filename }) { + const extension = filename.split('.').pop(); + + return ( +
+ + {filename} +
+ ); +} +``` + +### Folder Icons + +**Before:** + +```tsx +import { getFileTypeIconProps, FileIconType } from '@fluentui/react-file-type-icons'; + +const iconProps = getFileTypeIconProps({ type: FileIconType.folder, size: 48 }); +Folder; +``` + +**After:** + +```tsx +import { FileTypeIcon, FileIconType } from '@fluentui/react-file-type-icons'; + +; +``` + +## Incremental Migration + +**You don't have to migrate all code at once!** All v8 utilities work identically in v9: + +```tsx +import { + FileTypeIcon, // v9 component (new) + getFileTypeIconAsUrl, // v8 utility (still works) +} from '@fluentui/react-file-type-icons'; + +function MyComponent() { + return ( +
+ {/* Old code - still works */} + Document + + {/* New code - recommended */} + +
+ ); +} +``` + +## Benefits of the New Component API + +1. **Simpler** - No manual URL handling or img element construction +2. **Better TypeScript** - Full type safety with v9's type definitions +3. **Accessible** - Built-in alt text and ARIA support +4. **Optimized** - Automatic pixel ratio and format handling + +## Need Help? + +If you encounter issues: + +1. Check the [Storybook examples](https://aka.ms/fluentui-storybook) +2. Open an issue on [GitHub](https://github.com/microsoft/fluentui) diff --git a/packages/react-components/react-file-type-icons/library/README.md b/packages/react-components/react-file-type-icons/library/README.md new file mode 100644 index 00000000000000..b8eac1168c1740 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/README.md @@ -0,0 +1,104 @@ +# @fluentui/react-file-type-icons + +**File Type Icons components for [Fluent UI React](https://react.fluentui.dev/)** + +## Components + +### FileTypeIcon + +Displays an icon representing a file type based on its extension or a special type (like folder). Automatically selects the appropriate icon from the Fluent Design file type icon set. + +#### Features + +- 🎨 **100+ file type icons** - Supports all common file extensions +- 📏 **Multiple sizes** - 16, 20, 24, 32, 40, 48, 64, and 96 pixels +- 🖼️ **Format support** - SVG (default) or PNG +- 📁 **Special types** - Folder, list items, and other non-file icons +- ♿ **Accessible** - Proper alt text for screen readers + +## Usage + +### Basic Examples + +```tsx +import { FileTypeIcon, FileIconType } from '@fluentui/react-file-type-icons'; + +// File extension + + +// Special type + + +// Common file types + + + +``` + +### Sizes + +```tsx + + + + +``` + +### Image Format + +```tsx +// SVG (default, recommended) + + +// PNG (for legacy scenarios) + +``` + +### Custom CDN + +```tsx + +``` + +### Custom Styling + +```tsx + +``` + +## Migration from v8 + +If you're migrating from `@fluentui/react-file-type-icons` (v8), see the [Migration Guide](./MIGRATION.md). + +**v8:** + +```tsx +import { getFileTypeIconProps } from '@fluentui/react-file-type-icons'; +const iconProps = getFileTypeIconProps({ extension: 'docx', size: 48 }); +Document; +``` + +**v9:** + +```tsx +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; +; +``` + +## Utilities + +Underlying utilities are exported for advanced use cases: + +```tsx +import { getFileTypeIconProps, getFileTypeIconAsUrl, FileIconType } from '@fluentui/react-file-type-icons'; + +// Get icon URL +const url = getFileTypeIconAsUrl({ extension: 'docx', size: 48 }); + +// Get icon props +const props = getFileTypeIconProps({ extension: 'docx', size: 48 }); +``` + +## API + +See [Fluent UI Storybook](https://aka.ms/fluentui-storybook) for full API documentation and interactive examples. diff --git a/packages/react-components/react-file-type-icons/library/bundle-size/FileTypeIcon.fixture.js b/packages/react-components/react-file-type-icons/library/bundle-size/FileTypeIcon.fixture.js new file mode 100644 index 00000000000000..5e374a14cd3a02 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/bundle-size/FileTypeIcon.fixture.js @@ -0,0 +1,10 @@ +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; + +console.log(FileTypeIcon); + +export default { + name: 'FileTypeIcon', + props: { + extension: 'docx', + }, +}; diff --git a/packages/react-components/react-file-type-icons/library/config/api-extractor.json b/packages/react-components/react-file-type-icons/library/config/api-extractor.json new file mode 100644 index 00000000000000..8d482156d10d53 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json", + "mainEntryPointFilePath": "/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts" +} diff --git a/packages/react-components/react-file-type-icons/library/config/tests.js b/packages/react-components/react-file-type-icons/library/config/tests.js new file mode 100644 index 00000000000000..2e211ae9e21420 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/config/tests.js @@ -0,0 +1 @@ +/** Jest test setup file. */ diff --git a/packages/react-components/react-file-type-icons/library/eslint.config.js b/packages/react-components/react-file-type-icons/library/eslint.config.js new file mode 100644 index 00000000000000..ec2e7cb1fc479f --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [...fluentPlugin.configs['flat/react']]; diff --git a/packages/react-components/react-file-type-icons/library/etc/react-file-type-icons.api.md b/packages/react-components/react-file-type-icons/library/etc/react-file-type-icons.api.md new file mode 100644 index 00000000000000..ba285fb0bb30a4 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/etc/react-file-type-icons.api.md @@ -0,0 +1,155 @@ +## API Report File for "@fluentui/react-file-type-icons" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { IIconOptions } from '@fluentui/style-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import * as React_2 from 'react'; +import type { Slot } from '@fluentui/react-utilities'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +// @public (undocumented) +export const DEFAULT_BASE_URL = "https://res.cdn.office.net/files/fabric-cdn-prod_20251107.003/assets/item-types/"; + +// @public (undocumented) +export const DEFAULT_ICON_SIZE: FileTypeIconSize; + +// @public +export enum FileIconType { + // (undocumented) + album = 21,// Start at 1 so it will evaluate as "truthy" + // (undocumented) + desktopFolder = 9, + // (undocumented) + docset = 1, + // (undocumented) + documentsFolder = 10, + // (undocumented) + folder = 2, + // (undocumented) + form = 14, + // (undocumented) + genericFile = 3, + // (undocumented) + linkedFolder = 12, + // (undocumented) + list = 13, + // (undocumented) + listForm = 22, + // (undocumented) + listItem = 4, + // (undocumented) + loopworkspace = 17, + // (undocumented) + multiple = 6, + // (undocumented) + news = 8, + // (undocumented) + picturesFolder = 11, + // (undocumented) + planner = 18, + // (undocumented) + playlist = 16, + // (undocumented) + portfolio = 20, + // (undocumented) + sharedFolder = 5, + // (undocumented) + stream = 7, + // (undocumented) + sway = 15, + // (undocumented) + todoItem = 19 +} + +// @public +export const FileTypeIcon: ForwardRefComponent; + +// @public (undocumented) +export const fileTypeIconClassNames: SlotClassNames; + +// @public +export const FileTypeIconMap: { + [key: string]: { + extensions?: string[]; + types?: FileIconType[]; + }; +}; + +// @public (undocumented) +export interface FileTypeIconOptions { + extension?: string; + imageFileType?: ImageFileType; + size?: FileTypeIconSize; + type?: FileIconType; +} + +// @public (undocumented) +export type FileTypeIconProps = ComponentProps & { + extension?: string; + type?: FileIconType; + size?: FileTypeIconSize; + imageFileType?: ImageFileType; + baseUrl?: string; +}; + +// @public (undocumented) +export type FileTypeIconSize = 16 | 20 | 24 | 32 | 40 | 48 | 64 | 96; + +// @public (undocumented) +export type FileTypeIconSlots = { + root: Slot<'img'>; +}; + +// @public (undocumented) +export type FileTypeIconState = ComponentState & Required> & { + iconUrl: string; +}; + +// @public +export function getFileTypeIconAsHTMLString(options: FileTypeIconOptions, baseUrl?: string): string | undefined; + +// @public +export function getFileTypeIconAsUrl(options: FileTypeIconOptions, baseUrl?: string): string | undefined; + +// @public +export function getFileTypeIconNameFromExtensionOrType(extension: string | undefined, type: FileIconType | undefined): string; + +// @public +export function getFileTypeIconProps(options: FileTypeIconOptions): { + iconName: string; + 'aria-label'?: string; +}; + +// @public +export function getFileTypeIconSuffix(size: FileTypeIconSize | number, imageFileType?: ImageFileType, win?: Window): string; + +// @public +export function getValidIconSize(size: number): FileTypeIconSize; + +// @public (undocumented) +export const ICON_SIZES: number[]; + +// @public (undocumented) +export type ImageFileType = 'svg' | 'png'; + +// @public (undocumented) +export function initializeFileTypeIcons(baseUrl?: string, options?: Partial): void; + +// @public +export const renderFileTypeIcon_unstable: (state: FileTypeIconState) => JSXElement; + +// @public +export const useFileTypeIcon_unstable: (props: FileTypeIconProps, ref: React_2.Ref) => FileTypeIconState; + +// @public +export const useFileTypeIconStyles_unstable: (state: FileTypeIconState) => FileTypeIconState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-file-type-icons/library/jest.config.js b/packages/react-components/react-file-type-icons/library/jest.config.js new file mode 100644 index 00000000000000..8946bffacce655 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/jest.config.js @@ -0,0 +1,34 @@ +// @ts-check +/* eslint-disable */ + +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(join(__dirname, '.swcrc'), 'utf-8')); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-file-type-icons', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-file-type-icons/library/package.json b/packages/react-components/react-file-type-icons/library/package.json new file mode 100644 index 00000000000000..e7363b155f2426 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/package.json @@ -0,0 +1,57 @@ +{ + "name": "@fluentui/react-file-type-icons", + "version": "9.0.0", + "description": "Filetype icons for FluentUI", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "devDependencies": { + "@fluentui/eslint-plugin": "*", + "@fluentui/react-conformance": "*", + "@fluentui/react-conformance-griffel": "*", + "@fluentui/scripts-api-extractor": "*" + }, + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@fluentui/style-utilities": "^8.13.4", + "@fluentui/utilities": "^8.15.1", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.9.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "import": "./lib/index.js", + "require": "./lib-commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-file-type-icons/library/project.json b/packages/react-components/react-file-type-icons/library/project.json new file mode 100644 index 00000000000000..227a8e1ad8d90e --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-file-type-icons", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-file-type-icons/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-file-type-icons/library/src/FileTypeIcon.ts b/packages/react-components/react-file-type-icons/library/src/FileTypeIcon.ts new file mode 100644 index 00000000000000..a24037c4ce477b --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/FileTypeIcon.ts @@ -0,0 +1,8 @@ +export type { FileTypeIconProps, FileTypeIconSlots, FileTypeIconState } from './components/FileTypeIcon/index'; +export { + FileTypeIcon, + fileTypeIconClassNames, + renderFileTypeIcon_unstable, + useFileTypeIconStyles_unstable, + useFileTypeIcon_unstable, +} from './components/FileTypeIcon/index'; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.test.tsx b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.test.tsx new file mode 100644 index 00000000000000..9ac92d9431070f --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.test.tsx @@ -0,0 +1,174 @@ +import * as React from 'react'; +import { isConformant } from '../../testing/isConformant'; +import { render } from '@testing-library/react'; +import { FileTypeIcon } from './FileTypeIcon'; +import type { FileTypeIconProps } from './FileTypeIcon.types'; +import { FileIconType } from '../../utils/FileIconType'; +import { initializeFileTypeIcons, DEFAULT_BASE_URL } from '../../utils/initializeFileTypeIcons'; +import { resetConfiguredBaseUrl } from '../../testing'; + +describe('FileTypeIcon', () => { + isConformant({ + Component: FileTypeIcon, + displayName: 'FileTypeIcon', + }); + + it('renders with a file extension', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('docx'); + }); + + it('renders with a file type', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('folder'); + }); + + it('renders with custom size', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('48'); + }); + + it('renders with PNG image type', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('png'); + }); + + it('renders with SVG image type by default', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('svg'); + }); + + it('applies custom className', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img?.className).toContain('custom-class'); + }); + + it('renders with custom baseUrl', () => { + const customUrl = 'https://example.com/icons/'; + const result = render(); + const img = result.container.querySelector('img'); + expect(img?.src).toContain(customUrl); + }); + + it('handles file extensions with leading dot', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('pdf'); + }); + + it('defaults to genericfile for unknown extensions', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('genericfile'); + }); + + it('handles compound extensions like .tar.gz by using last extension', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + // gz maps to archive icon + expect(img?.src).toContain('archive'); + }); + + it('handles compound extensions like file.min.js', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + // js maps to code icon + expect(img?.src).toContain('code'); + }); +}); + +describe('FileTypeIcon size fallback', () => { + it('uses next smallest size when 30 is requested (falls back to 24)', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('/24/'); + }); + + it('uses next smallest size when 50 is requested (falls back to 48)', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('/48/'); + }); + + it('uses largest size (96) when size above maximum is requested', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('/96/'); + }); + + it('uses largest size (96) when size 128 is requested', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('/96/'); + }); + + it('uses smallest size (16) when size below minimum is requested', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img).toBeTruthy(); + expect(img?.src).toContain('/16/'); + }); +}); + +describe('FileTypeIcon with initializeFileTypeIcons', () => { + beforeEach(() => { + // Reset to default state before each test + resetConfiguredBaseUrl(); + }); + + afterAll(() => { + // Reset to default state after all tests + resetConfiguredBaseUrl(); + }); + + it('should use DEFAULT_BASE_URL when initializeFileTypeIcons has not been called', () => { + const result = render(); + const img = result.container.querySelector('img'); + expect(img?.src).toContain(DEFAULT_BASE_URL); + }); + + it('should use custom URL after initializeFileTypeIcons is called with custom URL', () => { + const customUrl = 'https://my-custom-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + + const result = render(); + const img = result.container.querySelector('img'); + expect(img?.src).toContain(customUrl); + }); + + it('should allow explicit baseUrl prop to override configured URL', () => { + const configuredUrl = 'https://configured-cdn.com/icons/'; + const explicitUrl = 'https://explicit-cdn.com/icons/'; + + initializeFileTypeIcons(configuredUrl); + + // Without explicit baseUrl - uses configured URL + const result1 = render(); + const img1 = result1.container.querySelector('img'); + expect(img1?.src).toContain(configuredUrl); + + // With explicit baseUrl - uses explicit URL + const result2 = render(); + const img2 = result2.container.querySelector('img'); + expect(img2?.src).toContain(explicitUrl); + }); +}); diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.tsx b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.tsx new file mode 100644 index 00000000000000..c0b5835360fcef --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.tsx @@ -0,0 +1,25 @@ +'use client'; + +import * as React from 'react'; +import { renderFileTypeIcon_unstable } from './renderFileTypeIcon'; +import { useFileTypeIcon_unstable } from './useFileTypeIcon'; +import { useFileTypeIconStyles_unstable } from './useFileTypeIconStyles.styles'; +import type { FileTypeIconProps } from './FileTypeIcon.types'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; + +/** + * FileTypeIcon component displays an icon representing a file type based on its extension or type. + * It supports various sizes and image formats (SVG and PNG). + */ +export const FileTypeIcon: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useFileTypeIcon_unstable(props, ref); + + useFileTypeIconStyles_unstable(state); + + useCustomStyleHook_unstable('useFileTypeIconStyles_unstable')(state); + + return renderFileTypeIcon_unstable(state); +}); + +FileTypeIcon.displayName = 'FileTypeIcon'; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.types.ts b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.types.ts new file mode 100644 index 00000000000000..01469d9cd2a8e0 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/FileTypeIcon.types.ts @@ -0,0 +1,52 @@ +import type { ComponentState, ComponentProps, Slot } from '@fluentui/react-utilities'; +import type { FileIconType } from '../../utils/FileIconType'; +import type { FileTypeIconSize, ImageFileType } from '../../utils/getFileTypeIconProps'; + +export type FileTypeIconSlots = { + /** + * The root slot, an img element that displays the file type icon. + */ + root: Slot<'img'>; +}; + +export type FileTypeIconProps = ComponentProps & { + /** + * The file extension, such as 'pptx' or '.docx', for which you need an icon. + * For file type icons that are not associated with a file extension, + * such as folder, use the type property. + */ + extension?: string; + + /** + * The type of file type icon you need. Use this property for + * file type icons that are not associated with a file extension, + * such as folder. + */ + type?: FileIconType; + + /** + * The size of the icon in pixels. + * @default 16 + */ + size?: FileTypeIconSize; + + /** + * The type of image file to use. Can be svg or png. + * @default 'svg' + */ + imageFileType?: ImageFileType; + + /** + * The base URL for the icon assets. If not provided, uses the default Fluent CDN. + * @default 'https://res.cdn.office.net/files/fabric-cdn-prod_20251119.001/assets/item-types/' + */ + baseUrl?: string; +}; + +export type FileTypeIconState = ComponentState & + Required> & { + /** + * The computed icon URL based on extension/type, size, and image file type. + */ + iconUrl: string; + }; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/index.ts b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/index.ts new file mode 100644 index 00000000000000..f70e9027521bc4 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/index.ts @@ -0,0 +1,5 @@ +export { FileTypeIcon } from './FileTypeIcon'; +export type { FileTypeIconProps, FileTypeIconSlots, FileTypeIconState } from './FileTypeIcon.types'; +export { renderFileTypeIcon_unstable } from './renderFileTypeIcon'; +export { useFileTypeIcon_unstable } from './useFileTypeIcon'; +export { fileTypeIconClassNames, useFileTypeIconStyles_unstable } from './useFileTypeIconStyles.styles'; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/renderFileTypeIcon.tsx b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/renderFileTypeIcon.tsx new file mode 100644 index 00000000000000..b0a81e57b70b58 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/renderFileTypeIcon.tsx @@ -0,0 +1,15 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { assertSlots } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import type { FileTypeIconSlots, FileTypeIconState } from './FileTypeIcon.types'; + +/** + * Render the FileTypeIcon component. + */ +export const renderFileTypeIcon_unstable = (state: FileTypeIconState): JSXElement => { + assertSlots(state); + + return ; +}; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIcon.ts b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIcon.ts new file mode 100644 index 00000000000000..abd7aeb7808215 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIcon.ts @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; +import type { FileTypeIconProps, FileTypeIconState } from './FileTypeIcon.types'; +import { + getFileTypeIconNameFromExtensionOrType, + getFileTypeIconSuffix, + getValidIconSize, + DEFAULT_ICON_SIZE, +} from '../../utils/getFileTypeIconProps'; +import { FileIconType } from '../../utils/FileIconType'; +import { getConfiguredBaseUrl } from '../../utils/initializeFileTypeIcons'; + +/** + * Returns the props and state required to render the FileTypeIcon component. + */ +export const useFileTypeIcon_unstable = ( + props: FileTypeIconProps, + ref: React.Ref, +): FileTypeIconState => { + const { + extension, + type, + size: requestedSize = DEFAULT_ICON_SIZE, + imageFileType = 'svg', + baseUrl = getConfiguredBaseUrl(), + } = props; + + // Validate and adjust size to nearest available + const size = getValidIconSize(requestedSize); + + // Get the base icon name and suffix separately using v8 pattern + const baseIconName = getFileTypeIconNameFromExtensionOrType(extension, type); + const baseSuffix = getFileTypeIconSuffix(size, imageFileType); + const suffixArray = baseSuffix.split('_'); // eg: ['96', '3x', 'svg'] or ['96', 'svg'] + + // Construct the full icon URL using v8's folder-based pattern + let iconUrl: string; + if (suffixArray.length === 3) { + // suffix is of type 96_3x_svg - it has a pixel ratio > 1 + iconUrl = `${baseUrl}${size}_${suffixArray[1]}/${baseIconName}.${suffixArray[2]}`; + } else if (suffixArray.length === 2) { + // suffix is of type 96_svg - it has a pixel ratio of 1 + iconUrl = `${baseUrl}${size}/${baseIconName}.${suffixArray[1]}`; + } else { + // Fallback to 1x format for unexpected cases + iconUrl = `${baseUrl}${size}/${baseIconName}.${imageFileType}`; + } + + // Generate alt text: use extension if provided, otherwise get the enum name for type + const altText = extension || (type !== undefined ? FileIconType[type] : ''); + + const state: FileTypeIconState = { + size, + imageFileType, + iconUrl, + components: { + root: 'img', + }, + root: slot.always( + getIntrinsicElementProps('img', { + ref, + src: iconUrl, + alt: `${altText} file icon`, + ...props, + // Remove our custom props from being passed to the img element + extension: undefined, + type: undefined, + baseUrl: undefined, + }), + { elementType: 'img' }, + ), + }; + + return state; +}; diff --git a/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIconStyles.styles.ts b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIconStyles.styles.ts new file mode 100644 index 00000000000000..cb3502c2a20218 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/components/FileTypeIcon/useFileTypeIconStyles.styles.ts @@ -0,0 +1,69 @@ +'use client'; + +import { mergeClasses, makeStyles } from '@griffel/react'; +import type { FileTypeIconSlots, FileTypeIconState } from './FileTypeIcon.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const fileTypeIconClassNames: SlotClassNames = { + root: 'fui-FileTypeIcon', +}; + +const useStyles = makeStyles({ + // Base styles + root: { + display: 'inline-block', + verticalAlign: 'middle', + }, + + // Size variations - ensure the icon displays at the correct size + size16: { + width: '16px', + height: '16px', + }, + size20: { + width: '20px', + height: '20px', + }, + size24: { + width: '24px', + height: '24px', + }, + size32: { + width: '32px', + height: '32px', + }, + size40: { + width: '40px', + height: '40px', + }, + size48: { + width: '48px', + height: '48px', + }, + size64: { + width: '64px', + height: '64px', + }, + size96: { + width: '96px', + height: '96px', + }, +}); + +/** + * Apply styling to the FileTypeIcon slots based on the state. + */ +export const useFileTypeIconStyles_unstable = (state: FileTypeIconState): FileTypeIconState => { + 'use no memo'; + + const styles = useStyles(); + + state.root.className = mergeClasses( + fileTypeIconClassNames.root, + styles.root, + styles[`size${state.size}`], + state.root.className, + ); + + return state; +}; diff --git a/packages/react-components/react-file-type-icons/library/src/index.ts b/packages/react-components/react-file-type-icons/library/src/index.ts new file mode 100644 index 00000000000000..8c33d5996d47fd --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/index.ts @@ -0,0 +1,23 @@ +// Component exports +export { + FileTypeIcon, + fileTypeIconClassNames, + renderFileTypeIcon_unstable, + useFileTypeIcon_unstable, + useFileTypeIconStyles_unstable, +} from './components/FileTypeIcon/index'; +export type { FileTypeIconProps, FileTypeIconSlots, FileTypeIconState } from './components/FileTypeIcon/index'; + +// Utility exports (for backward compatibility and advanced usage) +export { FileIconType } from './utils/FileIconType'; +export { FileTypeIconMap } from './utils/FileTypeIconMap'; +export { + getFileTypeIconProps, + getFileTypeIconNameFromExtensionOrType, + getFileTypeIconSuffix, + getValidIconSize, + DEFAULT_ICON_SIZE, +} from './utils/getFileTypeIconProps'; +export type { FileTypeIconSize, ImageFileType, FileTypeIconOptions } from './utils/getFileTypeIconProps'; +export { getFileTypeIconAsUrl, getFileTypeIconAsHTMLString } from './utils/getFileTypeIconAsUrl'; +export { initializeFileTypeIcons, DEFAULT_BASE_URL, ICON_SIZES } from './utils/initializeFileTypeIcons'; diff --git a/packages/react-components/react-file-type-icons/library/src/testing/index.ts b/packages/react-components/react-file-type-icons/library/src/testing/index.ts new file mode 100644 index 00000000000000..7ed58323b5b9a5 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/testing/index.ts @@ -0,0 +1,9 @@ +/** + * Testing utilities for @fluentui/react-file-type-icons + * + * These exports are intended for testing purposes only and should not be used in production code. + * They provide access to internal state management functions that allow tests to reset and verify + * the configured base URL state. + */ + +export { getConfiguredBaseUrl, resetConfiguredBaseUrl } from '../utils/initializeFileTypeIcons'; diff --git a/packages/react-components/react-file-type-icons/library/src/testing/isConformant.ts b/packages/react-components/react-file-type-icons/library/src/testing/isConformant.ts new file mode 100644 index 00000000000000..8ed2da0f925135 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/testing/isConformant.ts @@ -0,0 +1,15 @@ +import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; +import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; +import griffelTests from '@fluentui/react-conformance-griffel'; + +export function isConformant( + testInfo: Omit, 'componentPath'> & { componentPath?: string }, +): void { + const defaultOptions: Partial> = { + tsConfig: { configName: 'tsconfig.spec.json' }, + componentPath: require.main?.filename.replace('.test', ''), + extraTests: griffelTests as TestObject, + }; + + baseIsConformant(defaultOptions, testInfo); +} diff --git a/packages/react-components/react-file-type-icons/library/src/utils/FileIconType.ts b/packages/react-components/react-file-type-icons/library/src/utils/FileIconType.ts new file mode 100644 index 00000000000000..0a3b112acb3b00 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/FileIconType.ts @@ -0,0 +1,31 @@ +/** + * Enumerates special file type icons that do not map to any file extensions. + * For example, the 'pptx' icon maps to the extensions 'ppt', 'pptm', 'pptx', + * but the 'folder' icon does not map to any extensions and should be obtained + * via this enum. + */ + +export enum FileIconType { + docset = 1, // Start at 1 so it will evaluate as "truthy" + folder = 2, + genericFile = 3, + listItem = 4, + sharedFolder = 5, + multiple = 6, + stream = 7, + news = 8, + desktopFolder = 9, + documentsFolder = 10, + picturesFolder = 11, + linkedFolder = 12, + list = 13, + form = 14, + sway = 15, + playlist = 16, + loopworkspace = 17, + planner = 18, + todoItem = 19, + portfolio = 20, + album = 21, + listForm = 22, +} diff --git a/packages/react-components/react-file-type-icons/library/src/utils/FileTypeIconMap.ts b/packages/react-components/react-file-type-icons/library/src/utils/FileTypeIconMap.ts new file mode 100644 index 00000000000000..4f39e24ab25af3 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/FileTypeIconMap.ts @@ -0,0 +1,633 @@ +import { FileIconType } from './FileIconType'; + +/** + * Enumeration of icon file names, and what extensions they map to. + * Please keep items alphabetical. Items without extensions may require specific logic in the code to map. + * Always use getFileTypeIconProps to get the most up-to-date icon at the right pixel density. + */ +export const FileTypeIconMap: { [key: string]: { extensions?: string[]; types?: FileIconType[] } } = { + accdb: { + extensions: ['accdb', 'mdb'], + }, + aipage: { + extensions: ['page'], + }, + archive: { + extensions: ['7z', 'ace', 'arc', 'arj', 'dmg', 'gz', 'iso', 'lzh', 'pkg', 'rar', 'sit', 'tgz', 'tar', 'z'], + }, + album: { + types: [FileIconType.album], + }, + audio: { + extensions: [ + 'aif', + 'aiff', + 'aac', + 'alac', + 'amr', + 'ape', + 'au', + 'awb', + 'dct', + 'dss', + 'dvf', + 'flac', + 'gsm', + 'm4a', + 'm4p', + 'mid', + 'mmf', + 'mp3', + 'oga', + 'ra', + 'rm', + 'wav', + 'wma', + 'wv', + ], + }, + calendar: { + extensions: ['ical', 'icalendar', 'ics', 'ifb', 'vcs'], + }, + classifier: { + extensions: ['classifier'], + }, + clipchamp: { + extensions: ['clipchamp'], + }, + cliptemplate: { + extensions: ['cliptemplate'], + }, + code: { + extensions: [ + 'abap', + 'ada', + 'adp', + 'ahk', + 'as', + 'as3', + 'asc', + 'ascx', + 'asm', + 'asp', + 'awk', + 'bash', + 'bash_login', + 'bash_logout', + 'bash_profile', + 'bashrc', + 'bat', + 'bib', + 'bsh', + 'build', + 'builder', + 'c', + 'cbl', + 'c++', + 'capfile', + 'cc', + 'cfc', + 'cfm', + 'cfml', + 'cl', + 'clj', + 'cls', + 'cmake', + 'cmd', + 'coffee', + 'config', + 'cpp', + 'cpt', + 'cpy', + 'cs', + 'cshtml', + 'cson', + 'csproj', + 'css', + 'ctp', + 'cxx', + 'd', + 'ddl', + 'di', + 'disco', + 'dml', + 'dtd', + 'dtml', + 'el', + 'emakefile', + 'erb', + 'erl', + 'f', + 'f90', + 'f95', + 'fs', + 'fsi', + 'fsscript', + 'fsx', + 'gemfile', + 'gemspec', + 'gitconfig', + 'go', + 'groovy', + 'gvy', + 'h', + 'h++', + 'haml', + 'handlebars', + 'hbs', + 'hcp', + 'hh', + 'hpp', + 'hrl', + 'hs', + 'htc', + 'hxx', + 'idl', + 'iim', + 'inc', + 'inf', + 'ini', + 'inl', + 'ipp', + 'irbrc', + 'jade', + 'jav', + 'java', + 'js', + 'json', + 'jsp', + 'jsproj', + 'jsx', + 'l', + 'less', + 'lhs', + 'lisp', + 'log', + 'lst', + 'ltx', + 'lua', + 'm', + 'mak', + 'make', + 'manifest', + 'master', + 'md', + 'markdn', + 'markdown', + 'mdown', + 'mkdn', + 'ml', + 'mli', + 'mll', + 'mly', + 'mm', + 'mud', + 'nfo', + 'opml', + 'osascript', + 'p', + 'pas', + 'patch', + 'php', + 'php2', + 'php3', + 'php4', + 'php5', + 'phtml', + 'pl', + 'pm', + 'pod', + 'pp', + 'profile', + 'ps1', + 'ps1xml', + 'psd1', + 'psm1', + 'pss', + 'pt', + 'py', + 'pyw', + 'r', + 'rake', + 'rb', + 'rbx', + 'rc', + 'rdf', + 're', + 'reg', + 'rest', + 'resw', + 'resx', + 'rhtml', + 'rjs', + 'rprofile', + 'rpy', + 'rss', + 'rst', + 'ruby', + 'rxml', + 's', + 'sass', + 'scala', + 'scm', + 'sconscript', + 'sconstruct', + 'script', + 'scss', + 'sgml', + 'sh', + 'shtml', + 'sml', + 'svn-base', + 'swift', + 'sql', + 'sty', + 'tcl', + 'tex', + 'textile', + 'tld', + 'tli', + 'tmpl', + 'tpl', + 'vb', + 'vi', + 'vim', + 'vmg', + 'webpart', + 'wsp', + 'wsdl', + 'xhtml', + 'xoml', + 'xsd', + 'xslt', + 'yaml', + 'yaws', + 'yml', + 'zsh', + ], + }, + contact: { + extensions: ['vcf'], + }, + copilot: { + extensions: ['copilot'], + }, + /* css: {}, not broken out yet, snapping to 'code' for now */ + csv: { + extensions: ['csv'], + }, + designer: { + extensions: ['design'], + }, + desktopfolder: { + types: [FileIconType.desktopFolder], + }, + docset: { + types: [FileIconType.docset], + }, + documentsfolder: { + types: [FileIconType.documentsFolder], + }, + docx: { + extensions: ['doc', 'docm', 'docx', 'docb'], + }, + dotx: { + extensions: ['dot', 'dotm', 'dotx'], + }, + email: { + extensions: ['eml', 'msg', 'oft', 'ost', 'pst'], + }, + exe: { + extensions: ['application', 'appref-ms', 'apk', 'app', 'appx', 'exe', 'ipa', 'msi', 'xap'], + }, + favoritesfolder: {}, + folder: { + types: [FileIconType.folder], + }, + font: { + extensions: ['ttf', 'otf', 'woff'], + }, + form: { + types: [FileIconType.form], + }, + genericfile: { + types: [FileIconType.genericFile], + }, + html: { + extensions: ['htm', 'html', 'mht', 'mhtml'], + }, + ipynb: { + extensions: ['nnb', 'ipynb'], + }, + link: { + extensions: ['lnk', 'link', 'url', 'website', 'webloc'], + }, + linkedfolder: { + types: [FileIconType.linkedFolder], + }, + listform: { + types: [FileIconType.listForm], + }, + listitem: { + types: [FileIconType.listItem], + }, + loop: { + extensions: ['fluid', 'loop', 'note'], + }, + loopworkspace: { + types: [FileIconType.loopworkspace], + }, + officescript: { + extensions: ['osts'], + }, + splist: { + types: [FileIconType.list], + }, + mcworld: { + extensions: ['mcworld'], + }, + mctemplate: { + extensions: ['mctemplate'], + }, + model: { + extensions: [ + '3ds', + '3mf', + 'blend', + 'cool', + 'dae', + 'df', + 'dwfx', + 'dwg', + 'dxf', + 'fbx', + 'glb', + 'gltf', + 'holo', + 'layer', + 'layout', + 'max', + 'mtl', + 'obj', + 'off', + 'ply', + 'skp', + 'stp', + 'stl', + 't', + 'thl', + 'x', + ], + }, + mpp: { + extensions: ['mpp'], + }, + mpt: { + extensions: ['mpt'], + }, + multiple: { + types: [FileIconType.multiple], + }, + one: { + // This is a partial OneNote page or section export. Not whole notebooks, see "onetoc" + extensions: ['one', 'onepart'], + }, + onepage: { + extensions: ['onepage'], + }, + onetoc: { + // This is an entire OneNote notebook. + extensions: ['ms-one-stub', 'onetoc', 'onetoc2', 'onepkg'], // This represents a complete, logical notebook. + }, + pbiapp: {}, + pdf: { + extensions: ['pdf'], + }, + photo: { + extensions: [ + 'arw', + 'bmp', + 'cr2', + 'crw', + 'dic', + 'dicm', + 'dcm', + 'dcm30', + 'dcr', + 'dds', + 'dib', + 'dng', + 'erf', + 'gif', + 'heic', + 'heif', + 'ico', + 'jfi', + 'jfif', + 'jif', + 'jpe', + 'jpeg', + 'jpg', + 'jxr', + 'kdc', + 'mrw', + 'nef', + 'orf', + 'pct', + 'pict', + 'png', + 'pns', + 'psb', + 'psd', + 'raw', + 'tga', + 'tif', + 'tiff', + 'wdp', + ], + }, + photo360: {}, + picturesfolder: { + types: [FileIconType.picturesFolder], + }, + planner: { + types: [FileIconType.planner], + }, + portfolio: { + types: [FileIconType.portfolio], + }, + potx: { + extensions: ['pot', 'potm', 'potx'], + }, + powerbi: { + extensions: ['pbids', 'pbix'], + }, + ppsx: { + extensions: ['pps', 'ppsm', 'ppsx'], + }, + pptx: { + extensions: ['ppt', 'pptm', 'pptx', 'sldx', 'sldm'], + }, + presentation: { + extensions: ['odp', 'gslides', 'key'], + }, + pub: { + extensions: ['pub'], + }, + spo: { + extensions: ['aspx'], + }, + sponews: { + types: [FileIconType.news], + }, + spreadsheet: { + extensions: ['odc', 'ods', 'gsheet', 'numbers', 'tsv'], + }, + rtf: { + extensions: ['epub', 'gdoc', 'odt', 'rtf', 'wri', 'pages'], + }, + sharedfolder: { + types: [FileIconType.sharedFolder], + }, + playlist: { + types: [FileIconType.playlist], + }, + sway: { + types: [FileIconType.sway], + }, + sysfile: { + extensions: [ + 'bak', + 'bin', + 'cab', + 'cache', + 'cat', + 'cer', + 'class', + 'dat', + 'db', + 'dbg', + 'dl_', + 'dll', + 'ithmb', + 'jar', + 'kb', + 'ldt', + 'lrprev', + 'pkpass', + 'ppa', + 'ppam', + 'pdb', + 'rom', + 'thm', + 'thmx', + 'vsl', + 'xla', + 'xlam', + 'xlb', + 'xll', + ], + }, + todoitem: { + types: [FileIconType.todoItem], + }, + txt: { + extensions: ['dif', 'diff', 'readme', 'out', 'plist', 'properties', 'text', 'txt'], + }, + vaultclosed: {}, + vaultopen: {}, + vector: { + extensions: [ + 'ai', + 'ait', + 'cvs', + 'dgn', + 'gdraw', + 'pd', + 'emf', + 'eps', + 'fig', + 'ind', + 'indd', + 'indl', + 'indt', + 'indb', + 'ps', + 'svg', + 'svgz', + 'wmf', + 'oxps', + 'xps', + 'xd', + 'sketch', + ], + }, + video: { + types: [FileIconType.stream], + extensions: [ + '3g2', + '3gp', + '3gp2', + '3gpp', + 'asf', + 'avi', + 'dvr-ms', + 'flv', + 'm1v', + 'm4v', + 'mkv', + 'mod', + 'mov', + 'mm4p', + 'mp2', + 'mp2v', + 'mp4', + 'mp4v', + 'mpa', + 'mpe', + 'mpeg', + 'mpg', + 'mpv', + 'mpv2', + 'mts', + 'ogg', + 'qt', + 'swf', + 'ts', + 'vob', + 'webm', + 'wlmp', + 'wm', + 'wmv', + 'wmx', + ], + }, + video360: {}, + vsdx: { + extensions: ['vdx', 'vsd', 'vsdm', 'vsdx', 'vsw', 'vdw'], + }, + vssx: { + extensions: ['vss', 'vssm', 'vssx'], + }, + vstx: { + extensions: ['vst', 'vstm', 'vstx', 'vsx'], + }, + whiteboard: { + extensions: ['whiteboard', 'wbtx'], + }, + xlsx: { + extensions: ['xlc', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xlw'], + }, + xltx: { + extensions: ['xlt', 'xltm', 'xltx'], + }, + xml: { + extensions: ['xaml', 'xml', 'xsl'], + }, + xsn: { + extensions: ['xsn'], + }, + zip: { + extensions: ['zip'], + }, +}; diff --git a/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.test.ts b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.test.ts new file mode 100644 index 00000000000000..35f7f0d77b6de9 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.test.ts @@ -0,0 +1,453 @@ +import { getFileTypeIconAsUrl, getFileTypeIconAsHTMLString } from './getFileTypeIconAsUrl'; +import { DEFAULT_BASE_URL } from './initializeFileTypeIcons'; +import { getFileTypeIconNameFromExtensionOrType, getValidIconSize } from './getFileTypeIconProps'; +import { FileIconType } from './FileIconType'; + +describe('getFileTypeIconNameFromExtensionOrType', () => { + describe('with FileIconType enum values', () => { + it('should map all FileIconType enum values to specific icon names', () => { + // Test all 22 enum values + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.docset)).toBe('docset'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.folder)).toBe('folder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.genericFile)).toBe('genericfile'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.listItem)).toBe('listitem'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.sharedFolder)).toBe('sharedfolder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.multiple)).toBe('multiple'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.stream)).toBe('video'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.news)).toBe('sponews'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.desktopFolder)).toBe('desktopfolder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.documentsFolder)).toBe('documentsfolder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.picturesFolder)).toBe('picturesfolder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.linkedFolder)).toBe('linkedfolder'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.list)).toBe('splist'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.form)).toBe('form'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.sway)).toBe('sway'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.playlist)).toBe('playlist'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.loopworkspace)).toBe('loopworkspace'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.planner)).toBe('planner'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.todoItem)).toBe('todoitem'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.portfolio)).toBe('portfolio'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.album)).toBe('album'); + expect(getFileTypeIconNameFromExtensionOrType(undefined, FileIconType.listForm)).toBe('listform'); + }); + + it('should not return genericfile for any valid FileIconType enum', () => { + // Verify no enum value falls back to the default + const enumValues = [ + FileIconType.docset, + FileIconType.folder, + FileIconType.genericFile, + FileIconType.listItem, + FileIconType.sharedFolder, + FileIconType.multiple, + FileIconType.stream, + FileIconType.news, + FileIconType.desktopFolder, + FileIconType.documentsFolder, + FileIconType.picturesFolder, + FileIconType.linkedFolder, + FileIconType.list, + FileIconType.form, + FileIconType.sway, + FileIconType.playlist, + FileIconType.loopworkspace, + FileIconType.planner, + FileIconType.todoItem, + FileIconType.portfolio, + FileIconType.album, + FileIconType.listForm, + ]; + + enumValues.forEach(enumValue => { + const result = getFileTypeIconNameFromExtensionOrType(undefined, enumValue); + expect(result).toBeDefined(); + // Only FileIconType.genericFile should map to 'genericfile' + if (enumValue !== FileIconType.genericFile) { + expect(result).not.toBe('genericfile'); + } else { + expect(result).toBe('genericfile'); + } + }); + }); + }); + + describe('with compound extensions', () => { + it('should extract the last extension from .tar.gz', () => { + // .tar.gz should extract 'gz' and map to appropriate icon + const result = getFileTypeIconNameFromExtensionOrType('.tar.gz', undefined); + expect(result).toBe('archive'); // gz maps to archive icon + }); + + it('should extract the last extension from archive.tar.bz2', () => { + const result = getFileTypeIconNameFromExtensionOrType('archive.tar.bz2', undefined); + // bz2 is not a known extension, should fall back to genericfile + expect(result).toBe('genericfile'); + }); + + it('should extract the last extension from file.min.js', () => { + const result = getFileTypeIconNameFromExtensionOrType('file.min.js', undefined); + expect(result).toBe('code'); // js maps to code icon + }); + + it('should extract the last extension from styles.module.css', () => { + const result = getFileTypeIconNameFromExtensionOrType('styles.module.css', undefined); + expect(result).toBe('code'); // css maps to code icon + }); + + it('should handle simple extension with leading dot', () => { + const result = getFileTypeIconNameFromExtensionOrType('.docx', undefined); + expect(result).toBe('docx'); + }); + + it('should handle simple extension without leading dot', () => { + const result = getFileTypeIconNameFromExtensionOrType('pdf', undefined); + expect(result).toBe('pdf'); + }); + }); +}); + +describe('getValidIconSize', () => { + describe('with exact valid sizes', () => { + it('should return exact size when 16 is requested', () => { + expect(getValidIconSize(16)).toBe(16); + }); + + it('should return exact size when 20 is requested', () => { + expect(getValidIconSize(20)).toBe(20); + }); + + it('should return exact size when 24 is requested', () => { + expect(getValidIconSize(24)).toBe(24); + }); + + it('should return exact size when 32 is requested', () => { + expect(getValidIconSize(32)).toBe(32); + }); + + it('should return exact size when 40 is requested', () => { + expect(getValidIconSize(40)).toBe(40); + }); + + it('should return exact size when 48 is requested', () => { + expect(getValidIconSize(48)).toBe(48); + }); + + it('should return exact size when 64 is requested', () => { + expect(getValidIconSize(64)).toBe(64); + }); + + it('should return exact size when 96 is requested', () => { + expect(getValidIconSize(96)).toBe(96); + }); + }); + + describe('with sizes requiring fallback to smaller', () => { + it('should return 16 when 18 is requested (next smallest)', () => { + expect(getValidIconSize(18)).toBe(16); + }); + + it('should return 20 when 22 is requested (next smallest)', () => { + expect(getValidIconSize(22)).toBe(20); + }); + + it('should return 24 when 30 is requested (next smallest)', () => { + expect(getValidIconSize(30)).toBe(24); + }); + + it('should return 32 when 35 is requested (next smallest)', () => { + expect(getValidIconSize(35)).toBe(32); + }); + + it('should return 64 when 80 is requested (next smallest)', () => { + expect(getValidIconSize(80)).toBe(64); + }); + }); + + describe('with sizes larger than maximum', () => { + it('should return 96 when 100 is requested', () => { + expect(getValidIconSize(100)).toBe(96); + }); + + it('should return 96 when 128 is requested', () => { + expect(getValidIconSize(128)).toBe(96); + }); + + it('should return 96 when 256 is requested', () => { + expect(getValidIconSize(256)).toBe(96); + }); + }); + + describe('with sizes smaller than minimum', () => { + it('should return 16 when 10 is requested', () => { + expect(getValidIconSize(10)).toBe(16); + }); + + it('should return 16 when 8 is requested', () => { + expect(getValidIconSize(8)).toBe(16); + }); + + it('should return 16 when 1 is requested', () => { + expect(getValidIconSize(1)).toBe(16); + }); + }); +}); + +describe('getFileTypeIconAsUrl', () => { + // Store original DPR once at the start + const originalDPR = typeof window !== 'undefined' ? window.devicePixelRatio : 1; + + beforeEach(() => { + // Reset to 1x before each test + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1, + }); + } + }); + + afterAll(() => { + // Restore original DPR after all tests in this suite + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: originalDPR, + }); + } + }); + + describe('with different DPI values', () => { + it('should return correct URL for 1x DPI with docx extension', () => { + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 16, imageFileType: 'png' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16/docx.png`); + }); + + it('should return correct URL for 1.5x DPI with pdf extension', () => { + // Mock window with 1.5x DPI + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1.5, + }); + + const result = getFileTypeIconAsUrl({ extension: 'pdf', size: 16, imageFileType: 'png' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16_1.5x/pdf.png`); + }); + + it('should return correct URL for 2x DPI with xlsx extension', () => { + // Mock window with 2x DPI + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 2, + }); + + const result = getFileTypeIconAsUrl({ extension: 'xlsx', size: 16, imageFileType: 'png' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16_2x/xlsx.png`); + }); + }); + + describe('with SVG format', () => { + it('should return correct URL for 1x DPI with SVG', () => { + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 16, imageFileType: 'svg' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16/docx.svg`); + }); + + it('should return correct URL for 1.5x DPI with SVG', () => { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1.5, + }); + + const result = getFileTypeIconAsUrl({ extension: 'pdf', size: 16, imageFileType: 'svg' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16_1.5x/pdf.svg`); + }); + + it('should return correct URL for 2x DPI with SVG (should not include DPI suffix)', () => { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 2, + }); + + const result = getFileTypeIconAsUrl({ extension: 'xlsx', size: 16, imageFileType: 'svg' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}16/xlsx.svg`); + }); + }); + + describe('with different sizes', () => { + it('should return correct URL for size 20', () => { + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 20, imageFileType: 'png' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}20/docx.png`); + }); + + it('should return correct URL for size 24', () => { + const result = getFileTypeIconAsUrl({ extension: 'pdf', size: 24, imageFileType: 'svg' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}24/pdf.svg`); + }); + + it('should return correct URL for size 48', () => { + const result = getFileTypeIconAsUrl({ extension: 'xlsx', size: 48, imageFileType: 'png' }, DEFAULT_BASE_URL); + expect(result).toBe(`${DEFAULT_BASE_URL}48/xlsx.png`); + }); + }); + + describe('with custom base URL', () => { + const customBaseUrl = 'https://custom.cdn.com/icons/'; + + it('should use custom base URL', () => { + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 16, imageFileType: 'svg' }, customBaseUrl); + expect(result).toBe(`${customBaseUrl}16/docx.svg`); + }); + }); +}); + +describe('getFileTypeIconAsHTMLString', () => { + // Store original DPR once at the start + const originalDPR = typeof window !== 'undefined' ? window.devicePixelRatio : 1; + + beforeEach(() => { + // Reset to 1x before each test + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1, + }); + } + }); + + afterAll(() => { + // Restore original DPR after all tests in this suite + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: originalDPR, + }); + } + }); + + describe('with different DPI values', () => { + it('should return correct HTML string for 1x DPI with docx extension', () => { + const result = getFileTypeIconAsHTMLString( + { extension: 'docx', size: 16, imageFileType: 'png' }, + DEFAULT_BASE_URL, + ); + expect(result).toBe(`docx file icon`); + }); + + it('should return correct HTML string for 1.5x DPI with pdf extension', () => { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1.5, + }); + + const result = getFileTypeIconAsHTMLString( + { extension: 'pdf', size: 16, imageFileType: 'png' }, + DEFAULT_BASE_URL, + ); + expect(result).toBe( + `pdf file icon`, + ); + }); + + it('should return correct HTML string for 2x DPI with xlsx extension', () => { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 2, + }); + + const result = getFileTypeIconAsHTMLString( + { extension: 'xlsx', size: 16, imageFileType: 'png' }, + DEFAULT_BASE_URL, + ); + expect(result).toBe( + `xlsx file icon`, + ); + }); + }); + + describe('with SVG format', () => { + it('should return correct HTML string for 1x DPI with SVG', () => { + const result = getFileTypeIconAsHTMLString( + { extension: 'docx', size: 24, imageFileType: 'svg' }, + DEFAULT_BASE_URL, + ); + expect(result).toBe(`docx file icon`); + }); + + it('should return correct HTML string for 1.5x DPI with SVG', () => { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1.5, + }); + + const result = getFileTypeIconAsHTMLString( + { extension: 'pdf', size: 20, imageFileType: 'svg' }, + DEFAULT_BASE_URL, + ); + expect(result).toBe( + `pdf file icon`, + ); + }); + }); + + describe('with different sizes', () => { + it('should include correct size attributes for size 32', () => { + const result = getFileTypeIconAsHTMLString( + { extension: 'docx', size: 32, imageFileType: 'png' }, + DEFAULT_BASE_URL, + ); + expect(result).toContain('height="32"'); + expect(result).toContain('width="32"'); + expect(result).toContain('alt="docx file icon"'); + }); + + it('should include correct size attributes for size 48', () => { + const result = getFileTypeIconAsHTMLString( + { extension: 'xlsx', size: 48, imageFileType: 'svg' }, + DEFAULT_BASE_URL, + ); + expect(result).toContain('height="48"'); + expect(result).toContain('width="48"'); + expect(result).toContain('alt="xlsx file icon"'); + }); + }); + + describe('with custom base URL', () => { + const customBaseUrl = 'https://custom.cdn.com/icons/'; + + it('should use custom base URL in HTML string', () => { + const result = getFileTypeIconAsHTMLString({ extension: 'docx', size: 16, imageFileType: 'svg' }, customBaseUrl); + expect(result).toBe(`docx file icon`); + }); + }); + + describe('edge cases', () => { + it('should handle unknown extension gracefully with alt text', () => { + // Unknown extensions should still generate valid HTML with alt text + const result = getFileTypeIconAsHTMLString({ extension: 'unknown', size: 16 }, DEFAULT_BASE_URL); + expect(result).toBeDefined(); + expect(result).toContain('alt="unknown file icon"'); + }); + + it('should generate alt text from FileIconType when extension is not provided', () => { + const result = getFileTypeIconAsHTMLString({ type: FileIconType.folder, size: 16 }, DEFAULT_BASE_URL); + expect(result).toBeDefined(); + expect(result).toContain('alt="folder file icon"'); + }); + + it('should generate empty alt text prefix when neither extension nor type is provided', () => { + const result = getFileTypeIconAsHTMLString({ size: 16 }, DEFAULT_BASE_URL); + expect(result).toBeDefined(); + expect(result).toContain('alt=" file icon"'); + }); + }); +}); diff --git a/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.ts b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.ts new file mode 100644 index 00000000000000..d27885a86ef5d0 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconAsUrl.ts @@ -0,0 +1,59 @@ +import { getConfiguredBaseUrl } from './initializeFileTypeIcons'; +import { + getFileTypeIconNameFromExtensionOrType, + getFileTypeIconSuffix, + DEFAULT_ICON_SIZE, +} from './getFileTypeIconProps'; +import type { FileTypeIconOptions } from './getFileTypeIconProps'; +import { FileIconType } from './FileIconType'; + +/** + * Given the `fileTypeIconOptions`, this function returns the CDN-based URL for `FileTypeIcon`. + * Similar to `getFileTypeIconProps`, but rather than returning the `iconName`, this returns the raw URL. + * @param options - Provide extension, FileIconType, size, and imageFileType for the requested icon. + * @param baseUrl - optionally provide a custom CDN base url to fetch icons from. + * If not provided, uses the URL configured via `initializeFileTypeIcons()`, + * or falls back to the default CDN URL. + */ +export function getFileTypeIconAsUrl( + options: FileTypeIconOptions, + baseUrl: string = getConfiguredBaseUrl(), +): string | undefined { + const { extension, size = DEFAULT_ICON_SIZE, type, imageFileType = 'svg' } = options; + const baseIconName = getFileTypeIconNameFromExtensionOrType(extension, type); + const suffix = getFileTypeIconSuffix(size, imageFileType); + + // suffix format: {size}_{imageType} or {size}_{pixelRatio}_{imageType} + // Examples: "16_svg", "96_3x_svg", "20_1.5x_png" + const lastUnderscoreIndex = suffix.lastIndexOf('_'); + const fileExtension = suffix.substring(lastUnderscoreIndex + 1); + const pathPrefix = suffix.substring(0, lastUnderscoreIndex); // "16" or "96_3x" or "20_1.5x" + + // CDN path format: {baseUrl}{pathPrefix}/{iconName}.{fileExtension} + // Examples: baseUrl16/docx.svg, baseUrl96_3x/docx.svg + return `${baseUrl}${pathPrefix}/${baseIconName}.${fileExtension}`; +} + +/** + * Given the `fileTypeIconOptions`, similar to `getFileTypeIconProps`, this function returns + * an tag DOM element that renders the icon, as a string. + * @param options - Provide extension, FileIconType, size, and imageFileType for the requested icon. + * @param baseUrl - optionally provide a custom CDN base url to fetch icons from. + * If not provided, uses the URL configured via `initializeFileTypeIcons()`, + * or falls back to the default CDN URL. + */ +export function getFileTypeIconAsHTMLString( + options: FileTypeIconOptions, + baseUrl: string = getConfiguredBaseUrl(), +): string | undefined { + const url = getFileTypeIconAsUrl(options, baseUrl); + + if (!url) { + return undefined; + } + + const { size = DEFAULT_ICON_SIZE, extension, type } = options; + // Generate alt text: use extension if provided, otherwise get the enum name for type + const altText = extension || (type !== undefined ? FileIconType[type] : ''); + return `${altText} file icon`; +} diff --git a/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconProps.ts b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconProps.ts new file mode 100644 index 00000000000000..4e5545ff7839a7 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/getFileTypeIconProps.ts @@ -0,0 +1,193 @@ +import { FileTypeIconMap } from './FileTypeIconMap'; +import { FileIconType } from './FileIconType'; +import { ICON_SIZES } from './initializeFileTypeIcons'; + +let _extensionToIconName: { [key: string]: string }; +let _typeToIconName: { [key: number]: string }; + +const GENERIC_FILE = 'genericfile'; + +export const DEFAULT_ICON_SIZE: FileTypeIconSize = 16; +export type FileTypeIconSize = 16 | 20 | 24 | 32 | 40 | 48 | 64 | 96; +export type ImageFileType = 'svg' | 'png'; + +export interface FileTypeIconOptions { + /** + * The file extension, such as .pptx, for which you need an icon. + * For file type icons that are not associated with a file + * extension, such as folder, use the type property. + */ + extension?: string; + /** + * The type of file type icon you need. Use this property for + * file type icons that are not associated with a file extension, + * such as folder. + */ + type?: FileIconType; + /** + * The size of the icon in pixels. + * @default 16 + */ + size?: FileTypeIconSize; + /** + * The type of image file to use. Can be svg or png. + * @default 'svg' + */ + imageFileType?: ImageFileType; +} + +/** + * Gets the nearest valid icon size. If the requested size is not available, + * returns the next smallest available size. If the requested size is larger + * than all available sizes, returns the largest available size (96). + * If the requested size is smaller than all available sizes, returns the smallest (16). + * + * @param size - The requested icon size + * @returns The nearest valid icon size from ICON_SIZES + */ +export function getValidIconSize(size: number): FileTypeIconSize { + // ICON_SIZES is already sorted: [16, 20, 24, 32, 40, 48, 64, 96] + const sortedSizes = ICON_SIZES as FileTypeIconSize[]; + + // If exact match exists, return it + if (sortedSizes.includes(size as FileTypeIconSize)) { + return size as FileTypeIconSize; + } + + // If size is larger than the largest available, return the largest + if (size > sortedSizes[sortedSizes.length - 1]) { + return sortedSizes[sortedSizes.length - 1]; + } + + // If size is smaller than the smallest available, return the smallest + if (size < sortedSizes[0]) { + return sortedSizes[0]; + } + + // Find the next smallest available size + for (let i = sortedSizes.length - 1; i >= 0; i--) { + if (sortedSizes[i] <= size) { + return sortedSizes[i]; + } + } + + // Fallback to default (should never reach here) + return DEFAULT_ICON_SIZE; +} + +/** + * This function returns properties for a file type icon given the FileTypeIconOptions. + * It accounts for different device pixel ratios. For example, + * `getFileTypeIconProps({ extension: 'doc', size: 16, imageFileType: 'png' })` + * will return `{ iconName: 'docx16_2x_png' }` if the `devicePixelRatio` is 2. + * @param options - Configuration options for the file type icon + */ +export function getFileTypeIconProps(options: FileTypeIconOptions): { iconName: string; 'aria-label'?: string } { + // First, obtain the base name of the icon using the extension or type. + const { extension, type, size, imageFileType } = options; + + const iconBaseName = getFileTypeIconNameFromExtensionOrType(extension, type); + + // Next, obtain the suffix using the icon size, user's device pixel ratio, and + // preference for svg or png + const _size: FileTypeIconSize = size || DEFAULT_ICON_SIZE; + const suffix: string = getFileTypeIconSuffix(_size, imageFileType); + + return { iconName: iconBaseName + suffix, 'aria-label': extension }; +} + +/** + * Gets the base icon name from a file extension or file icon type. + * @param extension - The file extension (e.g., 'docx', 'pdf') + * @param type - The file icon type for non-extension icons + * @returns The base icon name + */ +export function getFileTypeIconNameFromExtensionOrType( + extension: string | undefined, + type: FileIconType | undefined, +): string { + if (extension) { + if (!_extensionToIconName) { + _extensionToIconName = {}; + + for (const iconName in FileTypeIconMap) { + if (FileTypeIconMap.hasOwnProperty(iconName)) { + const extensions = FileTypeIconMap[iconName].extensions; + + if (extensions) { + for (let i = 0; i < extensions.length; i++) { + _extensionToIconName[extensions[i]] = iconName; + } + } + } + } + } + + // Extract only the last extension (handles compound extensions like .tar.gz -> gz) + // and force lowercase + const lastDotIndex = extension.lastIndexOf('.'); + extension = (lastDotIndex >= 0 ? extension.substring(lastDotIndex + 1) : extension).toLowerCase(); + return _extensionToIconName[extension] || GENERIC_FILE; + } else if (type) { + if (!_typeToIconName) { + _typeToIconName = {}; + + for (const iconName in FileTypeIconMap) { + if (FileTypeIconMap.hasOwnProperty(iconName)) { + const types = FileTypeIconMap[iconName].types; + + if (types) { + for (let i = 0; i < types.length; i++) { + _typeToIconName[types[i]] = iconName; + } + } + } + } + } + + return _typeToIconName[type] || GENERIC_FILE; + } + + return GENERIC_FILE; +} + +/** + * Gets the suffix for the icon name based on size, file type, and device pixel ratio. + * If the requested size is not available, it will be adjusted to the nearest valid size. + * @param size - The icon size in pixels (will be validated against available sizes) + * @param imageFileType - The image file type ('svg' or 'png') + * @param win - Optional window object for testing + * @returns The icon name suffix + */ +export function getFileTypeIconSuffix( + size: FileTypeIconSize | number, + imageFileType: ImageFileType = 'svg', + win?: Window, +): string { + // Validate and adjust size to nearest available + const validSize = getValidIconSize(size); + + // eslint-disable-next-line @nx/workspace-no-restricted-globals + win ??= typeof window !== 'undefined' ? window : ({ devicePixelRatio: 1 } as Window); + const devicePixelRatio: number = win.devicePixelRatio; + let devicePixelRatioSuffix = ''; // Default is 1x + + // SVGs scale well, so you can generally use the default image. + if (imageFileType === 'svg' && devicePixelRatio > 1 && devicePixelRatio <= 1.9) { + // 1.5x is a special case where SVGs need a different image. + devicePixelRatioSuffix = '_1.5x'; + } else if (imageFileType === 'png') { + // To look good, PNGs should use a different image for higher device pixel ratios + if (devicePixelRatio > 1 && devicePixelRatio <= 1.5) { + devicePixelRatioSuffix = '_1.5x'; + } else if (devicePixelRatio > 1.5 && devicePixelRatio <= 2) { + devicePixelRatioSuffix = '_2x'; + } else if (devicePixelRatio > 2 && devicePixelRatio <= 3) { + devicePixelRatioSuffix = '_3x'; + } else if (devicePixelRatio > 3) { + devicePixelRatioSuffix = '_4x'; + } + } + + return validSize + devicePixelRatioSuffix + '_' + imageFileType; +} diff --git a/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.test.ts b/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.test.ts new file mode 100644 index 00000000000000..c94feed6a513ac --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.test.ts @@ -0,0 +1,145 @@ +import { initializeFileTypeIcons, DEFAULT_BASE_URL } from './initializeFileTypeIcons'; +import { getConfiguredBaseUrl, resetConfiguredBaseUrl } from '../testing'; +import { getFileTypeIconAsUrl, getFileTypeIconAsHTMLString } from './getFileTypeIconAsUrl'; + +describe('initializeFileTypeIcons', () => { + // Store original DPR once at the start + const originalDPR = typeof window !== 'undefined' ? window.devicePixelRatio : 1; + + beforeEach(() => { + // Reset to default state before each test + resetConfiguredBaseUrl(); + + // Reset to 1x DPR before each test + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: 1, + }); + } + }); + + afterAll(() => { + // Restore original DPR after all tests + if (typeof window !== 'undefined') { + Object.defineProperty(window, 'devicePixelRatio', { + writable: true, + configurable: true, + value: originalDPR, + }); + } + // Reset to default state + resetConfiguredBaseUrl(); + }); + + describe('getConfiguredBaseUrl', () => { + it('should return DEFAULT_BASE_URL when initializeFileTypeIcons has not been called', () => { + expect(getConfiguredBaseUrl()).toBe(DEFAULT_BASE_URL); + }); + + it('should return DEFAULT_BASE_URL after calling initializeFileTypeIcons with no arguments', () => { + initializeFileTypeIcons(); + expect(getConfiguredBaseUrl()).toBe(DEFAULT_BASE_URL); + }); + + it('should return custom URL after calling initializeFileTypeIcons with custom URL', () => { + const customUrl = 'https://my-custom-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + expect(getConfiguredBaseUrl()).toBe(customUrl); + }); + + it('should return DEFAULT_BASE_URL after resetConfiguredBaseUrl is called', () => { + const customUrl = 'https://my-custom-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + expect(getConfiguredBaseUrl()).toBe(customUrl); + + resetConfiguredBaseUrl(); + expect(getConfiguredBaseUrl()).toBe(DEFAULT_BASE_URL); + }); + }); + + describe('integration with getFileTypeIconAsUrl', () => { + it('should use DEFAULT_BASE_URL when initializeFileTypeIcons has not been called', () => { + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 16 }); + expect(result).toBe(`${DEFAULT_BASE_URL}16/docx.svg`); + }); + + it('should use custom URL after initializeFileTypeIcons is called with custom URL', () => { + const customUrl = 'https://my-custom-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + + const result = getFileTypeIconAsUrl({ extension: 'docx', size: 16 }); + expect(result).toBe(`${customUrl}16/docx.svg`); + }); + + it('should allow explicit baseUrl parameter to override configured URL', () => { + const configuredUrl = 'https://configured-cdn.com/icons/'; + const explicitUrl = 'https://explicit-cdn.com/icons/'; + + initializeFileTypeIcons(configuredUrl); + + // Without explicit baseUrl - uses configured URL + const resultWithoutExplicit = getFileTypeIconAsUrl({ extension: 'docx', size: 16 }); + expect(resultWithoutExplicit).toBe(`${configuredUrl}16/docx.svg`); + + // With explicit baseUrl - uses explicit URL + const resultWithExplicit = getFileTypeIconAsUrl({ extension: 'docx', size: 16 }, explicitUrl); + expect(resultWithExplicit).toBe(`${explicitUrl}16/docx.svg`); + }); + }); + + describe('integration with getFileTypeIconAsHTMLString', () => { + it('should use DEFAULT_BASE_URL when initializeFileTypeIcons has not been called', () => { + const result = getFileTypeIconAsHTMLString({ extension: 'pdf', size: 24 }); + expect(result).toBe(`pdf file icon`); + }); + + it('should use custom URL after initializeFileTypeIcons is called with custom URL', () => { + const customUrl = 'https://my-custom-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + + const result = getFileTypeIconAsHTMLString({ extension: 'pdf', size: 24 }); + expect(result).toBe(`pdf file icon`); + }); + + it('should allow explicit baseUrl parameter to override configured URL', () => { + const configuredUrl = 'https://configured-cdn.com/icons/'; + const explicitUrl = 'https://explicit-cdn.com/icons/'; + + initializeFileTypeIcons(configuredUrl); + + // Without explicit baseUrl - uses configured URL + const resultWithoutExplicit = getFileTypeIconAsHTMLString({ extension: 'xlsx', size: 32 }); + expect(resultWithoutExplicit).toContain(configuredUrl); + + // With explicit baseUrl - uses explicit URL + const resultWithExplicit = getFileTypeIconAsHTMLString({ extension: 'xlsx', size: 32 }, explicitUrl); + expect(resultWithExplicit).toContain(explicitUrl); + }); + }); + + describe('URL override persistence', () => { + it('should persist custom URL across multiple utility calls', () => { + const customUrl = 'https://persistent-cdn.com/icons/'; + initializeFileTypeIcons(customUrl); + + // Multiple calls should all use the configured URL + expect(getFileTypeIconAsUrl({ extension: 'docx', size: 16 })).toContain(customUrl); + expect(getFileTypeIconAsUrl({ extension: 'pdf', size: 24 })).toContain(customUrl); + expect(getFileTypeIconAsUrl({ extension: 'xlsx', size: 32 })).toContain(customUrl); + expect(getFileTypeIconAsHTMLString({ extension: 'pptx', size: 48 })).toContain(customUrl); + }); + + it('should update configured URL when initializeFileTypeIcons is called again', () => { + const firstUrl = 'https://first-cdn.com/icons/'; + const secondUrl = 'https://second-cdn.com/icons/'; + + initializeFileTypeIcons(firstUrl); + expect(getFileTypeIconAsUrl({ extension: 'docx', size: 16 })).toContain(firstUrl); + + initializeFileTypeIcons(secondUrl); + expect(getFileTypeIconAsUrl({ extension: 'docx', size: 16 })).toContain(secondUrl); + }); + }); +}); diff --git a/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.tsx b/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.tsx new file mode 100644 index 00000000000000..899dfbb006d599 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/src/utils/initializeFileTypeIcons.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { registerIcons, FLUENT_CDN_BASE_URL } from '@fluentui/style-utilities'; +import { FileTypeIconMap } from './FileTypeIconMap'; +import type { IIconOptions } from '@fluentui/style-utilities'; +import type { JSXElement } from '@fluentui/utilities'; + +const PNG_SUFFIX = '_png'; +const SVG_SUFFIX = '_svg'; + +export const DEFAULT_BASE_URL = `${FLUENT_CDN_BASE_URL}/assets/item-types/`; +export const ICON_SIZES: number[] = [16, 20, 24, 32, 40, 48, 64, 96]; + +// Module-level state to track the configured base URL +let _configuredBaseUrl: string | undefined; + +/** + * Returns the configured base URL if `initializeFileTypeIcons` was called with a custom URL, + * otherwise returns the default CDN base URL. + * This is used internally by FileTypeIcon component and utility functions. + */ +export function getConfiguredBaseUrl(): string { + return _configuredBaseUrl ?? DEFAULT_BASE_URL; +} + +/** + * Resets the configured base URL to undefined, causing `getConfiguredBaseUrl` to return + * the default CDN base URL. This is primarily intended for testing purposes. + */ +export function resetConfiguredBaseUrl(): void { + _configuredBaseUrl = undefined; +} + +export function initializeFileTypeIcons(baseUrl: string = DEFAULT_BASE_URL, options?: Partial): void { + // Store the configured base URL for use by v9 components and utilities + _configuredBaseUrl = baseUrl; + + ICON_SIZES.forEach((size: number) => { + _initializeIcons(baseUrl, size, options); + }); +} + +function _initializeIcons(baseUrl: string, size: number, options?: Partial): void { + const iconTypes: string[] = Object.keys(FileTypeIconMap); + const fileTypeIcons: { [key: string]: JSXElement } = {}; + + iconTypes.forEach((type: string) => { + const baseUrlSizeType = baseUrl + size + '/' + type; + fileTypeIcons[type + size + PNG_SUFFIX] = ; + fileTypeIcons[type + size + SVG_SUFFIX] = ; + + // For high resolution screens, register additional versions + // Apply height=100% and width=100% to force image to fit into containing element + + // SVGs scale well, so you can generally use the default image. + // 1.5x is a special case where both SVGs and PNGs need a different image. + + fileTypeIcons[type + size + '_1.5x' + PNG_SUFFIX] = ( + + ); + fileTypeIcons[type + size + '_1.5x' + SVG_SUFFIX] = ( + + ); + + fileTypeIcons[type + size + '_2x' + PNG_SUFFIX] = ( + + ); + fileTypeIcons[type + size + '_3x' + PNG_SUFFIX] = ( + + ); + fileTypeIcons[type + size + '_4x' + PNG_SUFFIX] = ( + + ); + }); + + registerIcons( + { + fontFace: {}, + style: { + width: size, + height: size, + overflow: 'hidden', + }, + icons: fileTypeIcons, + mergeImageProps: true, + }, + options, + ); +} diff --git a/packages/react-components/react-file-type-icons/library/tsconfig.json b/packages/react-components/react-file-type-icons/library/tsconfig.json new file mode 100644 index 00000000000000..32bdbdf1ac26f0 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/react-components/react-file-type-icons/library/tsconfig.lib.json b/packages/react-components/react-file-type-icons/library/tsconfig.lib.json new file mode 100644 index 00000000000000..53066fdd11fff0 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../../dist/out-tsc/types", + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-file-type-icons/library/tsconfig.spec.json b/packages/react-components/react-file-type-icons/library/tsconfig.spec.json new file mode 100644 index 00000000000000..911456fe4b4d91 --- /dev/null +++ b/packages/react-components/react-file-type-icons/library/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/packages/react-components/react-file-type-icons/stories/.storybook/main.js b/packages/react-components/react-file-type-icons/stories/.storybook/main.js new file mode 100644 index 00000000000000..67905c6bfe15f2 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.mdx', '../src/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-file-type-icons/stories/.storybook/preview.js b/packages/react-components/react-file-type-icons/stories/.storybook/preview.js new file mode 100644 index 00000000000000..98274ed0b8095f --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/.storybook/preview.js @@ -0,0 +1,9 @@ +import * as rootPreview from '../../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; + +export const tags = ['autodocs']; diff --git a/packages/react-components/react-file-type-icons/stories/.storybook/tsconfig.json b/packages/react-components/react-file-type-icons/stories/.storybook/tsconfig.json new file mode 100644 index 00000000000000..4cdd1ce9d006f1 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment"] + }, + "include": ["*.js"] +} diff --git a/packages/react-components/react-file-type-icons/stories/README.md b/packages/react-components/react-file-type-icons/stories/README.md new file mode 100644 index 00000000000000..4e07135387f9a6 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/README.md @@ -0,0 +1,17 @@ +# @fluentui/react-file-type-icons-stories + +Storybook stories for packages/react-components/react-file-type-icons + +## Usage + +To include within storybook specify stories globs: + +\`\`\`js +module.exports = { +stories: ['../packages/react-components/react-file-type-icons/stories/src/**/*.mdx', '../packages/react-components/react-file-type-icons/stories/src/**/index.stories.@(ts|tsx)'], +} +\`\`\` + +## API + +no public API available diff --git a/packages/react-components/react-file-type-icons/stories/eslint.config.js b/packages/react-components/react-file-type-icons/stories/eslint.config.js new file mode 100644 index 00000000000000..8d88b5e748085d --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/eslint.config.js @@ -0,0 +1,12 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [ + ...fluentPlugin.configs['flat/react'], + { + rules: { + 'import/no-extraneous-dependencies': ['error', { packageDir: ['.', '../../../../'] }], + }, + }, +]; diff --git a/packages/react-components/react-file-type-icons/stories/package.json b/packages/react-components/react-file-type-icons/stories/package.json new file mode 100644 index 00000000000000..880429588b2e1c --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/package.json @@ -0,0 +1,13 @@ +{ + "name": "@fluentui/react-file-type-icons-stories", + "version": "0.0.0", + "private": true, + "devDependencies": { + "@fluentui/react-components": "*", + "@fluentui/react-file-type-icons": "*", + "@fluentui/react-storybook-addon": "*", + "@fluentui/react-storybook-addon-export-to-sandbox": "*", + "@fluentui/scripts-storybook": "*", + "@fluentui/eslint-plugin": "*" + } +} diff --git a/packages/react-components/react-file-type-icons/stories/project.json b/packages/react-components/react-file-type-icons/stories/project.json new file mode 100644 index 00000000000000..0a48172868e0f1 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-file-type-icons-stories", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-file-type-icons/stories/src", + "tags": ["vNext", "platform:web", "type:stories"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-file-type-icons/stories/src/.gitkeep b/packages/react-components/react-file-type-icons/stories/src/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconBestPractices.md b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconBestPractices.md new file mode 100644 index 00000000000000..f09ce54e1b468b --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconBestPractices.md @@ -0,0 +1,22 @@ +# Best practices + +- **Use SVG format (default) for better scalability and smaller file sizes** - SVG icons scale perfectly at any size and are more performant than PNG. +- **Choose appropriate sizes based on UI density**: + - Use 16-20px for compact interfaces, breadcrumbs, and dense lists + - Use 24-32px for standard list items and cards + - Use 40-48px for featured content and file pickers + - Use 64-96px only for hero sections or file upload zones +- **Provide meaningful context through surrounding UI** - File type icons work best when combined with file names, metadata, or labels. This is especially valuable in search results or similar experiences where diverse item types may be present. +- **Use special types for non-file entities** - Use `folder`, `sharedFolder`, `listItem`, `docset`, or `genericFile` types for appropriate contexts instead of trying to force file extensions. +- **Handle unknown file extensions gracefully** - The component automatically falls back to `genericFile` for unrecognized extensions, and is regularly updated to support new file types if they have a recognized icon. +- **Consider accessibility** - The component provides default alt text, but ensure the surrounding context makes the file's purpose clear to all users. +- **Use consistent sizes within the same UI context** - Mixing different icon sizes in a single list or grid creates visual inconsistency. + +## Things to avoid + +- **Don't use file type icons as the sole means of identifying files** - Always pair icons with file names or descriptions for better usability and accessibility. +- **Don't use file type icons as primary navigation elements** - These icons are meant to represent file types, not actions or navigation destinations. +- **Don't override the default alt text without good reason** - The component provides sensible defaults based on the file extension or type. +- **Don't use very large sizes (64px+) in dense lists or tables** - Large icons create excessive whitespace and reduce content density. Use thumbnail previews of the real file contents if available. +- **Don't use file type icons for branding or decorative purposes** - These icons follow Fluent Design System conventions and are meant for functional file type representation. Avoid non-square aspect ratios, custom styling overrides or distorting the visual design. +- **Don't assume all file types have unique icons** - Many extensions may share the same icon; rely on file names for precise identification. diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconCommon.stories.tsx b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconCommon.stories.tsx new file mode 100644 index 00000000000000..4acc77589980be --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconCommon.stories.tsx @@ -0,0 +1,146 @@ +/* eslint-disable @fluentui/no-restricted-imports */ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import type { JSXElement } from '@fluentui/react-components'; +import { FileTypeIcon, FileIconType } from '@fluentui/react-file-type-icons'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + gap: '32px', + }, + categorySection: { + display: 'flex', + flexDirection: 'column', + gap: '12px', + }, + categoryTitle: { + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + marginBottom: '8px', + }, + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', + gap: '16px', + }, + item: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '8px', + padding: '12px', + backgroundColor: tokens.colorNeutralBackground1, + borderRadius: tokens.borderRadiusMedium, + ':hover': { + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + label: { + fontSize: tokens.fontSizeBase200, + textAlign: 'center', + color: tokens.colorNeutralForeground2, + }, + specialTypesDescription: { + marginBottom: '12px', + color: tokens.colorNeutralForeground3, + }, +}); + +const fileTypeCategories = { + documents: ['docx', 'xlsx', 'pdf', 'txt', 'rtf', 'odt', 'pptx', 'csv'], + media: ['jpg', 'svg', 'mp4', 'mp3', 'wav', 'aac'], + code: ['html', 'url', 'json', 'xml', 'py', 'java', 'cpp'], + data: ['zip', 'tar', 'sql', 'accdb'], +}; + +const specialTypes = [ + { type: FileIconType.folder, label: 'Folder' }, + { type: FileIconType.genericFile, label: 'Generic File' }, + { type: FileIconType.sharedFolder, label: 'Shared Folder' }, + { type: FileIconType.listItem, label: 'List Item' }, + { type: FileIconType.docset, label: 'Docset' }, +]; + +export const CommonFileTypes = (): JSXElement => { + const styles = useStyles(); + + return ( +
+
+
Common Documents
+
+ {fileTypeCategories.documents.map(ext => ( +
+ +
.{ext}
+
+ ))} +
+
+ +
+
Media Files
+
+ {fileTypeCategories.media.map(ext => ( +
+ +
.{ext}
+
+ ))} +
+
+ +
+
Web & Programming
+
+ {fileTypeCategories.code.map(ext => ( +
+ +
.{ext}
+
+ ))} +
+
+ +
+
Data Files
+
+ {fileTypeCategories.data.map(ext => ( +
+ +
.{ext}
+
+ ))} +
+
+ +
+
Special Types
+

+ These types are not based on file extensions, but represent objects like folders and list items. +

+
+ {specialTypes.map(({ type, label }) => ( +
+ +
{label}
+
+ ))} +
+
+
+ ); +}; + +CommonFileTypes.parameters = { + docs: { + description: { + story: + 'FileTypeIcon supports a comprehensive library of file extensions organized by category (documents, media, code, data) and special types that are not based on file extensions (folder, shared folder, list item, docset, generic file).', + }, + }, +}; + +CommonFileTypes.storyName = 'File Types'; diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDefault.stories.tsx b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDefault.stories.tsx new file mode 100644 index 00000000000000..8f150560d7b118 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDefault.stories.tsx @@ -0,0 +1,94 @@ +/* eslint-disable @fluentui/no-restricted-imports */ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import type { JSXElement } from '@fluentui/react-components'; +import { FileIconType, FileTypeIcon } from '@fluentui/react-file-type-icons'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + gap: '16px', + }, + iconGroup: { + display: 'flex', + alignItems: 'center', + gap: '16px', + }, + label: { + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + color: tokens.colorNeutralForeground2, + minWidth: '40px', + }, + description: { + fontSize: tokens.fontSizeBase100, + color: tokens.colorNeutralForeground3, + }, +}); + +export const Default = (): JSXElement => { + const styles = useStyles(); + + return ( +
+
+ +
16px
+
+ Compact UIs, toolbars, dense tables. Example shows a Word document filetype icon. +
+
+
+ +
20px
+
+ Compact lists, navigation items. Example shows a folder, referenced via FileIconType. +
+
+
+ +
24px
+
Standard list items, search results. Example shows a Word document.
+
+
+ +
32px
+
+ Standard cards, file browsers. Example shows a PowerPoint presentation with a legacy file extension. +
+
+
+ +
40px
+
Featured items, file pickers. Example shows a video file.
+
+
+ +
48px
+
+ Grid views, attachment previews. Example shows a Microsoft Lists object. +
+
+
+ +
64px
+
Large grid items, file upload zones. Text file.
+
+
+ +
96px
+
Hero sections, large previews. Example shows a shared folder.
+
+
+ ); +}; + +Default.parameters = { + docs: { + description: { + story: + 'FileTypeIcon supports 8 size variants (16, 20, 24, 32, 40, 48, 64, and 96 pixels) to accommodate different UI contexts. Choose smaller sizes for compact interfaces and larger sizes for featured content or file upload experiences.', + }, + }, +}; diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDescription.md b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDescription.md new file mode 100644 index 00000000000000..1674419d5c509c --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconDescription.md @@ -0,0 +1,23 @@ +# FileTypeIcon + +Filetype icons represent a file or other "digital object" based on its extension or a special type (like folder). This Fluent UI design system component provides consistent, recognizable visual representations of the user's items and documents across your application, aligned with Microsoft 365. + +The component automatically selects the appropriate icon from the comprehensive Fluent Design file type icon set and handles device pixel ratio for optimal display quality on all screens. + +## Use Filetype Icons when you need to: + +- **Display file lists or grids** - Help users quickly identify file types in recent lists, document libraries, file browsers, or search results +- **Show file attachments or file upload interfaces** - Indicate attachment types in emails, messages, or forms. Provide visual feedback about accepted or uploaded file types +- **Represent documents in workflows** - Show file types in approval processes, cloud content management interfaces, document workflows, or collaboration tools + +This control integrates seamlessly with other Fluent UI v9 components using the same design principles. It can be used in `DataGrid`, `List`, and `Card` components for displaying file collections, following consistent theming, spacing and sizing patterns with the broader Fluent Design System. + +## Features + +- **Automatic icon selection**: Matches file extensions to the appropriate icon from a comprehensive library +- **Multiple sizes**: Supports 16, 20, 24, 32, 40, 48, 64, and 96 pixel sizes for different UI contexts +- **Format support**: Renders icons as SVG (default, recommended) or PNG for maximum compatibility +- **Special types**: Supports non-file-based icons like folders, shared folders, list items, docsets, and generic files +- **Accessibility**: Includes appropriate alt text for screen readers and follows WCAG guidelines +- **Device-aware rendering**: Automatically handles different pixel densities for crisp display on all screens +- **Customizable base URL**: Configure a custom CDN or asset path for icon resources diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconEdgeCases.stories.tsx b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconEdgeCases.stories.tsx new file mode 100644 index 00000000000000..e5e2e61ffb3a36 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconEdgeCases.stories.tsx @@ -0,0 +1,249 @@ +/* eslint-disable @fluentui/no-restricted-imports */ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import type { JSXElement } from '@fluentui/react-components'; +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + gap: '32px', + }, + section: { + display: 'flex', + flexDirection: 'column', + gap: '12px', + }, + sectionTitle: { + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + marginBottom: '8px', + }, + description: { + fontSize: tokens.fontSizeBase300, + color: tokens.colorNeutralForeground3, + marginBottom: '8px', + }, + exampleGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', + gap: '16px', + }, + exampleItem: { + display: 'flex', + alignItems: 'center', + gap: '12px', + padding: '12px', + backgroundColor: tokens.colorNeutralBackground1, + borderRadius: tokens.borderRadiusMedium, + }, + label: { + fontSize: tokens.fontSizeBase300, + fontFamily: tokens.fontFamilyMonospace, + }, + fallbackNote: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + fontStyle: 'italic', + }, + warningBox: { + padding: '12px 16px', + backgroundColor: tokens.colorPaletteYellowBackground2, + borderLeft: `4px solid ${tokens.colorPaletteYellowBorder1}`, + borderRadius: tokens.borderRadiusMedium, + fontSize: tokens.fontSizeBase300, + }, + codeBlock: { + backgroundColor: tokens.colorNeutralBackground3, + padding: '12px', + borderRadius: tokens.borderRadiusMedium, + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase200, + overflowX: 'auto', + }, + truncatedText: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + flex: 1, + }, +}); + +export const EdgeCases = (): JSXElement => { + const styles = useStyles(); + + return ( +
+
+
Unknown Extensions
+
+ When FileTypeIcon encounters an unknown or unsupported file extension, it automatically falls back to the + generic file icon, ensuring consistent visual representation. +
+
+
+ +
+
.xyz123
+
→ genericFile
+
+
+
+ +
+
.unknown
+
→ genericFile
+
+
+
+ +
+
.custom
+
→ genericFile
+
+
+
+ +
+
.proprietary
+
→ genericFile
+
+
+
+
+ +
+
Empty or Null Extensions
+
+ When no extension is provided or the extension is empty, the component gracefully handles the case by showing + the generic file icon. +
+
+
+ +
+
extension=""
+
→ genericFile
+
+
+
+ +
+
no extension prop
+
→ genericFile
+
+
+
+
+ +
+
Special Characters in Extensions
+
+ Extensions with special characters, numbers, or unusual formatting are handled gracefully, though they + typically fall back to the generic icon if not recognized. +
+
+
+ +
+
.file.bak
+
Handles dots
+
+
+
+ +
+
.tar.gz
+
Double extension
+
+
+
+ +
+
.123
+
Numeric only
+
+
+
+ +
+
.v2
+
Alphanumeric
+
+
+
+
+ +
+
Very Long File Names
+
+ The icon component focuses on the extension, not the filename length. Regardless of filename length, the + correct icon is displayed. However, UI layouts should handle text truncation appropriately. +
+
+ +
+
+ This_is_a_very_long_filename_that_could_cause_layout_issues_in_some_contexts_Q4_2025_Final_Report_v3.pdf +
+
+
+
+ Note: While FileTypeIcon handles any extension length, consider truncating long filenames in + your UI layout to maintain readability and prevent overflow issues. +
+
+ +
+
Case Sensitivity and Periods
+
+ File extensions are handled in a case-insensitive manner. The component recognizes extensions regardless of + capitalization. Periods before the extension will be ignored. +
+
+
+ +
PDF
+
+
+ +
pdf
+
+
+ +
.Pdf
+
+
+
+ All variations display the same PDF icon. If you want to further sanitize your code, consider extracting and + normalizing file extensions from full filenames: +
+
+          
+            {`// Extract extension from filename
+const getExtension = (filename: string): string => {
+  const parts = filename.split('.');
+  return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
+};
+
+// Usage
+const filename = "Report.Final.PDF";
+const extension = getExtension(filename); // "pdf"
+
+`}
+          
+        
+
+
+ ); +}; + +EdgeCases.parameters = { + docs: { + description: { + story: + 'FileTypeIcon gracefully handles edge cases including unknown extensions, empty values, special characters, and case variations. The component falls back to a generic file icon for unrecognized extensions, ensuring your UI always displays something meaningful.', + }, + }, +}; diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconUrlAndHtml.stories.tsx b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconUrlAndHtml.stories.tsx new file mode 100644 index 00000000000000..9af847edfcc730 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/FileTypeIconUrlAndHtml.stories.tsx @@ -0,0 +1,225 @@ +/* eslint-disable @fluentui/no-restricted-imports */ +import * as React from 'react'; +import { makeStyles, tokens } from '@fluentui/react-components'; +import type { JSXElement } from '@fluentui/react-components'; +import { getFileTypeIconAsUrl, getFileTypeIconAsHTMLString } from '@fluentui/react-file-type-icons'; + +const useStyles = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + gap: '24px', + }, + section: { + display: 'flex', + flexDirection: 'column', + gap: '12px', + padding: '16px', + border: `1px solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + }, + sectionTitle: { + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + marginBottom: '8px', + }, + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: '16px', + }, + card: { + display: 'flex', + flexDirection: 'column', + gap: '8px', + padding: '12px', + backgroundColor: tokens.colorNeutralBackground2, + borderRadius: tokens.borderRadiusSmall, + }, + cardTitle: { + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + marginBottom: '4px', + }, + iconPreview: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '12px', + backgroundColor: tokens.colorNeutralBackground1, + borderRadius: tokens.borderRadiusSmall, + minHeight: '60px', + }, + code: { + fontSize: tokens.fontSizeBase100, + fontFamily: tokens.fontFamilyMonospace, + backgroundColor: tokens.colorNeutralBackground1, + padding: '8px', + borderRadius: tokens.borderRadiusSmall, + overflowX: 'auto', + wordBreak: 'break-all', + }, + label: { + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + fontWeight: tokens.fontWeightMedium, + }, +}); + +const commonFileTypes = ['docx', 'pdf', 'xlsx']; + +export const UrlAndHtml = (): JSXElement => { + const styles = useStyles(); + const [devicePixelRatio, setDevicePixelRatio] = React.useState('N/A'); + + React.useEffect(() => { + setDevicePixelRatio(window.devicePixelRatio); + }, []); + + return ( +
+ {/* URL Function Demo */} +
+
getFileTypeIconAsUrl() - PNG Format with Different DPIs
+
+ {commonFileTypes.map(extension => ( +
+
.{extension}
+ {(['1x', '1.5x', '2x'] as const).map(dpi => { + const url = getFileTypeIconAsUrl({ + extension, + size: 48, + imageFileType: 'png', + }); + + // For demo purposes, we'll show what the URL would be for each DPI + // In real usage, the browser's devicePixelRatio would determine this + const dpiSuffix = dpi === '1x' ? '' : `_${dpi}`; + const demoUrl = url?.replace(/48/, `48${dpiSuffix}`); + + return ( +
+
{dpi} DPI:
+
{demoUrl}
+
+ ); + })} +
+ ))} +
+
+ + {/* SVG URL Demo */} +
+
getFileTypeIconAsUrl() - SVG Format with Different DPIs
+
+ {commonFileTypes.map(extension => ( +
+
.{extension}
+ {(['1x', '1.5x', '2x'] as const).map(dpi => { + const url = getFileTypeIconAsUrl({ + extension, + size: 48, + imageFileType: 'svg', + }); + + // SVG only uses 1.5x for specific DPI ranges, otherwise uses base + const dpiSuffix = dpi === '1.5x' ? '_1.5x' : ''; + const demoUrl = url?.replace(/48/, `48${dpiSuffix}`); + + return ( +
+
{dpi} DPI:
+
{demoUrl}
+
+ ); + })} +
+ ))} +
+
+ + {/* HTML String Demo */} +
+
getFileTypeIconAsHTMLString() - Visual Preview
+
+ {commonFileTypes.map(extension => ( +
+
.{extension}
+
+
+
+
HTML Output:
+
+ {getFileTypeIconAsHTMLString({ + extension, + size: 48, + imageFileType: 'svg', + })} +
+
+ ))} +
+
+ + {/* Comparison at Different Sizes */} +
+
getFileTypeIconAsHTMLString() - Different Sizes
+
+ {([16, 24, 48] as const).map(size => ( +
+
Size: {size}px
+ {commonFileTypes.map(extension => ( +
+
.{extension}:
+
+
+
+
+ ))} +
+ ))} +
+
+ + {/* Interactive Example */} +
+
Current Device Information
+
Device Pixel Ratio: {devicePixelRatio}
+
+ This affects which icon variant is loaded for PNG images. SVG images scale better but may use 1.5x variant for + better rendering. +
+
+
+ ); +}; + +UrlAndHtml.parameters = { + docs: { + description: { + story: + 'Demonstrates the `getFileTypeIconAsUrl()` and `getFileTypeIconAsHTMLString()` utility functions with different DPI settings (1x, 1.5x, 2x) and common file types (docx, pdf, xlsx). These functions are useful when you need direct access to CDN URLs or HTML markup for file type icons.', + }, + }, +}; + +UrlAndHtml.storyName = 'URL and HTML Functions'; diff --git a/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/index.stories.tsx b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/index.stories.tsx new file mode 100644 index 00000000000000..043aecf909fe74 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/FileTypeIcon/index.stories.tsx @@ -0,0 +1,58 @@ +/* eslint-disable @fluentui/no-restricted-imports */ +import type { Meta } from '@storybook/react'; +import { FileTypeIcon } from '@fluentui/react-file-type-icons'; + +import descriptionMd from './FileTypeIconDescription.md'; +import bestPracticesMd from './FileTypeIconBestPractices.md'; + +export { Default } from './FileTypeIconDefault.stories'; +export { CommonFileTypes } from './FileTypeIconCommon.stories'; +export { UrlAndHtml } from './FileTypeIconUrlAndHtml.stories'; +export { EdgeCases } from './FileTypeIconEdgeCases.stories'; + +export default { + title: 'Icons/Filetype Icons', + component: FileTypeIcon, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + }, + }, + }, + argTypes: { + size: { + control: { type: 'select' }, + options: [16, 20, 24, 32, 40, 48, 64, 96], + description: 'The size of the icon in pixels', + table: { + type: { summary: '16 | 20 | 24 | 32 | 40 | 48 | 64 | 96' }, + defaultValue: { summary: '16' }, + }, + }, + extension: { + control: { type: 'text' }, + description: 'The file extension (without the dot)', + table: { + type: { summary: 'string' }, + }, + }, + type: { + control: { type: 'select' }, + options: ['folder', 'genericFile', 'sharedFolder', 'listItem', 'docset'], + description: 'Special icon type (alternative to extension)', + table: { + type: { summary: 'FileIconType' }, + }, + }, + imageFileType: { + control: { type: 'radio' }, + options: ['svg', 'png'], + description: 'The image format to use', + table: { + type: { summary: "'svg' | 'png'" }, + defaultValue: { summary: "'svg'" }, + }, + }, + }, +} as Meta; diff --git a/packages/react-components/react-file-type-icons/stories/src/index.ts b/packages/react-components/react-file-type-icons/stories/src/index.ts new file mode 100644 index 00000000000000..cb0ff5c3b541f6 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-components/react-file-type-icons/stories/tsconfig.json b/packages/react-components/react-file-type-icons/stories/tsconfig.json new file mode 100644 index 00000000000000..efc50169d1df18 --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/react-file-type-icons/stories/tsconfig.lib.json b/packages/react-components/react-file-type-icons/stories/tsconfig.lib.json new file mode 100644 index 00000000000000..9486b224643d9f --- /dev/null +++ b/packages/react-components/react-file-type-icons/stories/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2019", "dom"], + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md b/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md index 32323a2d71971f..6443cf2b761338 100644 --- a/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md +++ b/packages/react-components/react-shared-contexts/library/etc/react-shared-contexts.api.md @@ -97,6 +97,7 @@ export const CustomStyleHooksContext_unstable: React_2.Context = () => { diff --git a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Breadcrumb.Example.tsx b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Breadcrumb.Example.tsx index 32298d06aa8348..8c497b96c6d559 100644 --- a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Breadcrumb.Example.tsx +++ b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Breadcrumb.Example.tsx @@ -52,7 +52,7 @@ export class ChicletBreadcrumbExample extends React.Component { diff --git a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Example.tsx b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Example.tsx index 1a3cf023fe8e72..672f3543e68ba2 100644 --- a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Example.tsx +++ b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Example.tsx @@ -8,7 +8,7 @@ export const ChicletXsmallExample: React.FunctionComponent<{}> = () => { diff --git a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Footer.Example.tsx b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Footer.Example.tsx index 331c3dcc2f698c..84de367d89bbf2 100644 --- a/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Footer.Example.tsx +++ b/packages/react-examples/src/react-experiments/Chiclet/Chiclet.Xsmall.Footer.Example.tsx @@ -51,7 +51,7 @@ export const ChicletXsmallFooterExample: React.FunctionComponent<{}> = () => { }>{ITEMS[3].activity}} foreground={