diff --git a/change/@fluentui-web-components-abaf1559-b67f-4856-be2a-b793a70ad376.json b/change/@fluentui-web-components-abaf1559-b67f-4856-be2a-b793a70ad376.json new file mode 100644 index 0000000000000..5db1a685da791 --- /dev/null +++ b/change/@fluentui-web-components-abaf1559-b67f-4856-be2a-b793a70ad376.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "docs: update storybook and CEM docs\"", + "packageName": "@fluentui/web-components", + "email": "rupertdavid@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/.storybook/dedent.d.ts b/packages/web-components/.storybook/dedent.d.ts new file mode 100644 index 0000000000000..c517d6301c4f3 --- /dev/null +++ b/packages/web-components/.storybook/dedent.d.ts @@ -0,0 +1,6 @@ +declare module 'dedent' { + function dedent(strings: TemplateStringsArray, ...values: unknown[]): string; + function dedent(value: string): string; + + export = dedent; +} diff --git a/packages/web-components/.storybook/docs-root.css b/packages/web-components/.storybook/docs-root.css index 3bda6c5cae729..d19eb641a89c9 100644 --- a/packages/web-components/.storybook/docs-root.css +++ b/packages/web-components/.storybook/docs-root.css @@ -257,13 +257,28 @@ border-bottom: 1px solid #edebe9; } -#storybook-docs .docblock-argstable-body > tr > td:nth-child(4) { +#storybook-docs .docblock-argstable-body > tr > td[colspan] { + background: var(--colorNeutralBackground2) !important; +} + +/** + * Hides the Control column from the ArgsTable + * Disabled + * + */ +/* +#storybook-docs .docblock-argstable-body>tr>td:nth-child(4) { display: none; } -#storybook-docs .docblock-argstable-head > tr > th:nth-child(4) { +#storybook-docs .docblock-argstable-head>tr>th:nth-child(4) { display: none; } +*/ + +#storybook-docs .docblock-argstable-body > tr > td { + width: unset !important; +} #storybook-docs .docblock-argstable tbody tr { border: none; @@ -371,14 +386,6 @@ letter-spacing: -0.01em; } -#storybook-docs .docblock-argstable tr > :nth-child(1) { - width: 4%; -} - -#storybook-docs .docblock-argstable tr > :nth-child(2) { - width: 100%; -} - #storybook-docs .os-padding { z-index: 0; } diff --git a/packages/web-components/.storybook/preview.mjs b/packages/web-components/.storybook/preview.mjs index 2913b43e5b12c..785e03891c6e2 100644 --- a/packages/web-components/.storybook/preview.mjs +++ b/packages/web-components/.storybook/preview.mjs @@ -2,10 +2,24 @@ import { teamsDarkTheme, teamsLightTheme, webDarkTheme, webLightTheme } from '@f import * as prettier from 'prettier'; import prettierPluginHTML from 'prettier/parser-html.js'; import webcomponentsTheme from './theme.mjs'; +import { setStorybookHelpersConfig } from './wc-toolkit-helpers.js'; import '../src/index-rollup.js'; import './docs-root.css'; +// Load the Custom Elements Manifest for Storybook helpers +// @ts-ignore β€” JSON import attribute not needed in Vite, TS NodeNext is overly strict here +import customElementsManifest from '../custom-elements.json'; +// @ts-ignore β€” Storybook global +window.__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__ = customElementsManifest; + +// Configure CEM-based argTypes generation +setStorybookHelpersConfig({ + typeRef: 'parsedType', + hideArgRef: true, + categoryOrder: ['attributes', 'properties', 'slots', 'cssProps', 'cssParts', 'cssStates', 'methods', 'events'], +}); + const FAST_EXPRESSION_COMMENTS = //g; // Matches comments that contain FAST expressions const themes = { @@ -77,7 +91,7 @@ export const decorators = [ export const parameters = { layout: 'fullscreen', - controls: { expanded: true }, + controls: { expanded: true, sort: 'none' }, viewMode: 'docs', previewTabs: { canvas: { hidden: true }, diff --git a/packages/web-components/.storybook/tsconfig.json b/packages/web-components/.storybook/tsconfig.json index 9d912126a0e86..c1ee4022c8c6e 100644 --- a/packages/web-components/.storybook/tsconfig.json +++ b/packages/web-components/.storybook/tsconfig.json @@ -8,5 +8,5 @@ "noEmit": true, "types": ["node"] }, - "include": ["*", "../public", "../src/**/*.stories.*"] + "include": ["*", "./*.d.ts", "../public", "../src/**/*.stories.*"] } diff --git a/packages/web-components/.storybook/wc-toolkit-helpers.ts b/packages/web-components/.storybook/wc-toolkit-helpers.ts new file mode 100644 index 0000000000000..d50e2b45de18a --- /dev/null +++ b/packages/web-components/.storybook/wc-toolkit-helpers.ts @@ -0,0 +1,693 @@ +/** + * Storybook helpers for web components β€” derived from @wc-toolkit/storybook-helpers. + * + * This is a Lit-free fork that provides argTypes/args generation from the Custom Elements Manifest + * without any dependency on Lit or template rendering. Template rendering is handled separately + * by the FAST-based `renderComponent()` helper in `helpers.stories.ts`. + * + * @see https://github.com/wc-toolkit/storybook-helpers + * @license MIT + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { ArgTypes } from '@storybook/html'; +import { action } from 'storybook/actions'; + +// Minimal types from the Custom Elements Manifest spec (custom-elements-manifest/schema.d.ts). +// Inlined to avoid NodeNext resolution issues with the package's main: schema.json. +type CEMPackage = { + modules?: Array<{ + path: string; + declarations?: any[]; + exports?: Array<{ kind: string; declaration: { name: string }; name?: string }>; + typeDefinitionPath?: string; + }>; +}; + +type CssCustomProperty = { + name: string; + description?: string; + default?: string; + type?: { text: string }; +}; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ControlOptions = + | 'text' + | 'radio' + | 'select' + | 'boolean' + | 'number' + | 'color' + | 'date' + | 'object' + | 'file' + | 'inline-radio' + | 'check' + | 'inline-check' + | 'multi-select'; + +export type Categories = + | 'attributes' + | 'cssParts' + | 'cssProps' + | 'cssStates' + | 'events' + | 'methods' + | 'properties' + | 'slots'; + +export type StorybookHelpersOptions = { + /** hides the `arg ref` label on each control */ + hideArgRef?: boolean; + /** sets the custom type reference in the Custom Elements Manifest */ + typeRef?: string; + /** renders default values for attributes and CSS properties */ + renderDefaultValues?: boolean; + /** Category order */ + categoryOrder?: Array; +}; + +export type StoryOptions = { + /** Categories to exclude from these stories */ + excludeCategories?: Array; +}; + +export type StoryHelpers = { + /** Default args for the component stories */ + args: Partial & { [key: string]: any }; + /** ArgTypes configuration for Storybook controls */ + argTypes: ArgTypes; + /** Function to log events emitted by the component in the "Actions" panel */ + logEvent: (eventName: string, event: Event) => void; + /** List of custom events emitted by the component */ + events: string[]; +}; + +// --------------------------------------------------------------------------- +// CEM Utilities (inlined from @wc-toolkit/cem-utilities to avoid extra dep) +// --------------------------------------------------------------------------- + +/** + * Extended component type from the Custom Elements Manifest. + */ +type Component = Record & { + name: string; + tagName?: string; + customElement?: boolean; + modulePath?: string; + definitionPath?: string; + typeDefinitionPath?: string; + attributes?: any[]; + members?: any[]; + events?: any[]; + slots?: any[]; + cssProperties?: CssCustomProperty[]; + cssParts?: any[]; + cssStates?: any[]; +}; + +const DOM_EVENTS = new Set([ + 'AnimationEvent', + 'BeforeUnloadEvent', + 'ClipboardEvent', + 'CommandEvent', + 'DragEvent', + 'ErrorEvent', + 'Event', + 'FocusEvent', + 'FormDataEvent', + 'HashChangeEvent', + 'InputEvent', + 'KeyboardEvent', + 'MessageEvent', + 'MouseEvent', + 'MutationObserver', + 'NavigateEvent', + 'NavigationCurrentEntryChangeEvent', + 'PageRevealEvent', + 'PageSwapEvent', + 'PageTransitionEvent', + 'PointerEvent', + 'PopStateEvent', + 'ProgressEvent', + 'PromiseRejectionEvent', + 'StorageEvent', + 'SubmitEvent', + 'ToggleEvent', + 'TouchEvent', + 'TrackEvent', + 'TransitionEvent', + 'UIEvent', + 'WebGLContextEvent', + 'WheelEvent', +]); + +let cachedComponents: Component[] = []; +let cachedManifest: unknown; +const definitionExports = new Map(); + +function areObjectsEqual(obj1: unknown, obj2: unknown): boolean { + if (obj1 === obj2) return true; + if (obj1 === null || obj2 === null || typeof obj1 !== 'object' || typeof obj2 !== 'object') return false; + if (Array.isArray(obj1) && Array.isArray(obj2)) { + if (obj1.length !== obj2.length) return false; + return obj1.every((item, index) => areObjectsEqual(item, obj2[index])); + } + const keys1 = Object.keys(obj1 as object); + const keys2 = Object.keys(obj2 as object); + if (keys1.length !== keys2.length) return false; + if (!keys2.every(key => key in (obj1 as object))) return false; + return keys1.every(key => { + const val1 = (obj1 as Record)[key]; + const val2 = (obj2 as Record)[key]; + if (val1 === null && val2 === null) return true; + if (val1 === null || val2 === null) return false; + if (typeof val1 === 'object' && typeof val2 === 'object') return areObjectsEqual(val1, val2); + return val1 === val2; + }); +} + +function setAllDefinitionExports(customElementsManifest?: any) { + if (!customElementsManifest?.modules?.length) return; + (customElementsManifest as CEMPackage).modules?.forEach((mod: any) => { + const defExports = mod?.exports?.filter((e: any) => e.kind === 'custom-element-definition'); + defExports?.forEach((e: any) => { + if (e.declaration.name) { + definitionExports.set(e.declaration.name, mod.path); + } + }); + }); +} + +function getAllComponents(customElementsManifest?: unknown): Component[] { + if (!customElementsManifest) return []; + if (areObjectsEqual(customElementsManifest as object, cachedManifest as object)) { + return cachedComponents; + } + cachedComponents = []; + cachedManifest = customElementsManifest; + definitionExports.clear(); + setAllDefinitionExports(customElementsManifest); + + (customElementsManifest as CEMPackage).modules?.forEach((module: any) => { + const declarations = + module.declarations?.filter((d: any) => { + const ce = d as unknown as Component; + if (!ce.tagName || !ce.customElement) return false; + ce.modulePath = module.path; + ce.definitionPath = definitionExports.get(ce.name); + if ('typeDefinitionPath' in module && module.typeDefinitionPath) { + ce.typeDefinitionPath = (module as any).typeDefinitionPath as string; + } + return true; + }) ?? []; + cachedComponents.push(...(declarations as unknown as Component[])); + }); + + return cachedComponents; +} + +function getComponentByTagName(customElementsManifest?: unknown, tagName?: string): Component | undefined { + return getAllComponents(customElementsManifest).find(c => c?.tagName === tagName); +} + +function getComponentPublicMethods(component?: Component): any[] { + if (!component || !component.members) return []; + const getParameter = (p: any) => p.name + getParamType(p) + getParamDefaultValue(p); + const getParamType = (p: any) => (p.type?.text ? `${p.optional ? '?' : ''}: ${p.type?.text}` : ''); + const getParamDefaultValue = (p: any) => (p.default ? ` = ${p.default}` : ''); + + return ( + ( + component?.members?.filter( + member => + member.kind === 'method' && + member.privacy !== 'private' && + member.privacy !== 'protected' && + !member.name.startsWith('#'), + ) as any[] + )?.map(m => { + m.type = { + text: `${m.name}(${m.parameters?.map((p: any) => getParameter(p)).join(', ') || ''}) => ${ + m.return?.type?.text || 'void' + }`, + }; + return m; + }) || [] + ); +} + +function getComponentEventsWithType(component?: any): any[] { + if (!component || !component.events) return []; + return ( + component.events.map((e: any) => { + const type: string = e.type?.text; + const eventType = DOM_EVENTS.has(type) + ? type + : type && type !== 'CustomEvent' + ? `CustomEvent<${type}>` + : 'CustomEvent'; + return { ...e, type: { text: eventType } }; + }) || [] + ); +} + +function removeQuotes(value: string) { + return value.trim().replace(/^["'](.+(?=["']$))["']$/, '$1'); +} + +function getMemberDescription(description?: string, deprecated?: boolean | string) { + if (!deprecated) return description || ''; + const desc = description ? `- ${description}` : ''; + return typeof deprecated === 'string' ? `@deprecated ${deprecated} ${desc}` : `@deprecated ${desc}`; +} + +function toStorySlotName(slotName?: string): string { + const normalized = (slotName || 'default').trim().toLowerCase(); + + if (!normalized || normalized === 'default') { + return 'slottedContent'; + } + + const words = normalized.split(/[-_\s]+/).filter(Boolean); + const [first, ...rest] = words; + + return `${first}${rest.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('')}SlottedContent`; +} + +// --------------------------------------------------------------------------- +// CEM Parser (inlined from @wc-toolkit/storybook-helpers/cem-parser) +// --------------------------------------------------------------------------- + +type ArgSet = { + resets?: ArgTypes; + args: ArgTypes; +}; + +function getOptions(): StorybookHelpersOptions { + return (globalThis as any)?.__WC_STORYBOOK_HELPERS_CONFIG__ || {}; +} + +export function getAttributesAndProperties( + component?: Component, + enabled = true, +): { + resets?: ArgTypes; + propArgs: ArgTypes; + attrArgs: ArgTypes; +} { + const resets: ArgTypes = {}; + const attrArgs: ArgTypes = {}; + const propArgs: ArgTypes = {}; + + component?.members?.forEach(member => { + if (member.kind !== 'field') return; + + const attribute = component.attributes?.find((x: any) => member.name === x.fieldName); + const propName = member.name; + const args = attribute ? attrArgs : propArgs; + + resets[propName] = { name: propName, table: { disable: true } }; + + if (member.privacy === 'private' || member.privacy === 'protected' || member.static) return; + + const name = attribute?.name || member.name; + const opts = getOptions(); + const type = opts.typeRef ? (member as any)[`${opts.typeRef}`]?.text || member?.type?.text : member?.type?.text; + const propType = cleanUpType(type); + const defaultValue = member.readonly ? undefined : removeQuotes(member.default || ''); + const control = getControl(propType, attribute !== undefined); + + args[name] = { + name, + description: getDescription(member.description, propName, member.deprecated as string), + defaultValue: defaultValue + ? defaultValue === "''" + ? '' + : control === 'object' + ? JSON.parse(formatToValidJson(defaultValue)) + : defaultValue + : undefined, + control: enabled && !member.readonly && control ? { type: control } : false, + table: { + category: attribute ? 'attributes' : 'properties', + defaultValue: { summary: defaultValue }, + type: { summary: type }, + }, + }; + + const values = propType?.split('|'); + if (values && values.length > 1) { + args[name].options = values.map(x => removeQuotes(x)!); + } + }); + + return { resets, propArgs, attrArgs }; +} + +export function getCssProperties(component?: Component): ArgSet { + const resets: ArgTypes = {}; + const args: ArgTypes = {}; + + component?.cssProperties?.forEach(part => { + resets[part.name] = { name: part.name, table: { disable: true } }; + }); + + component?.cssProperties?.forEach(property => { + args[property.name] = { + name: property.name, + description: property.description, + defaultValue: property.default, + control: false, + table: { category: 'css custom properties' }, + }; + }); + + return { resets, args }; +} + +export function getCssParts(component?: Component): ArgSet { + const resets: ArgTypes = {}; + const args: ArgTypes = {}; + + component?.cssParts?.forEach(part => { + resets[part.name] = { name: part.name, table: { disable: true } }; + + args[`${part.name}-part`] = { + name: part.name, + description: part.description, + control: false, + table: { category: 'css parts', type: {} }, + }; + }); + + return { resets, args }; +} + +export function getCssStates(component?: Component): ArgSet { + const resets: ArgTypes = {}; + const args: ArgTypes = {}; + + component?.cssStates?.forEach(state => { + resets[state.name] = { name: state.name, table: { disable: true } }; + + args[`${state.name}-state`] = { + name: state.name, + description: state.description, + control: false, + table: { category: 'css states' }, + }; + }); + + return { resets, args }; +} + +export function getSlots(component?: Component): ArgSet { + const resets: ArgTypes = {}; + const args: ArgTypes = {}; + + component?.slots?.forEach(slot => { + resets[slot.name] = { name: slot.name, table: { disable: true } }; + + const slotName = slot.name || 'default'; + const storySlotName = toStorySlotName(slotName); + + args[storySlotName] = { + name: slotName === 'default' ? '' : slotName, + description: slot.description || (slotName === 'default' ? 'The default slot' : `The ${slotName} slot`), + control: false, + table: { category: 'slots', type: {} }, + }; + }); + + return { resets, args }; +} + +export function getEvents(component?: Component): ArgSet { + const args: ArgTypes = {}; + const resets: ArgTypes = {}; + + component?.events?.forEach(event => { + resets[event.name] = { name: event.name, table: { disable: true } }; + }); + + const events = getComponentEventsWithType(component); + events?.forEach(event => { + args[`${event.name}-event`] = { + name: event.name, + description: event.description, + control: false, + table: { + category: 'events', + type: { summary: event.type.text }, + }, + }; + }); + + return { resets, args }; +} + +export function getMethods(component?: Component): ArgSet { + const args: ArgTypes = {}; + const methods = getComponentPublicMethods(component); + + methods?.forEach(method => { + // Skip `Changed` observer methods for FAST components. + if (method.name.endsWith('Changed')) { + return; + } + + args[`${method.name}-method`] = { + name: method.name, + description: method.description, + control: false, + table: { + category: 'methods', + type: { summary: method.type.text }, + }, + }; + }); + + return { args }; +} + +// --------------------------------------------------------------------------- +// Control type inference +// --------------------------------------------------------------------------- + +function getControl(type: string, isAttribute = false): ControlOptions { + if (!type) return 'text'; + const lowerType = type.toLowerCase(); + const options = lowerType + .split('|') + .map(x => x.trim()) + .filter(x => x !== '' && x !== 'null' && x !== 'undefined'); + + if (isObject(lowerType) && !isAttribute) return 'object'; + if (hasType(options, 'boolean')) return 'boolean'; + if (hasType(options, 'number') && !hasType(options, 'string')) return 'number'; + if (hasType(options, 'date')) return 'date'; + return options.length > 1 ? 'select' : 'text'; +} + +function isObject(type: string) { + return ( + type.includes('array') || type.includes('object') || type.includes('{') || type.includes('[') || type.includes('<') + ); +} + +function hasType(values: string[] = [], type: string) { + return values?.find(value => value === type) !== undefined; +} + +function cleanUpType(type?: string): string { + return !type + ? '' + : type + .replace(' | undefined', '') + .replace(' | null', '') + .replace(' | void', '') + .replace(' | any', '') + .replace(' | unknown', '') + .replace(' | string & {}', '|') + .replace(' | (string & {})', '|') + .replace(' | string', '|') + .replace(' | number', '|') + .replace(' | boolean', '|') + .replace(' | object', '|') + .replace(' | Function', '|') + .replace(' | {}', '|') + .replace(' | []', '|'); +} + +function getDescription(description?: string, argRef?: string, deprecated?: string) { + let desc = getMemberDescription(description, deprecated); + const opts = getOptions(); + return opts.hideArgRef || !argRef ? desc : (desc += `\n\n\narg ref - \`${argRef}\``); +} + +function formatToValidJson(input: string): string { + return input + .replace(/'([^']+)'/g, '"$1"') + .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":') + .replace(/,\s*(}|])/g, '$1'); +} + +// --------------------------------------------------------------------------- +// Event logging (no Lit dependency) +// --------------------------------------------------------------------------- + +/** + * Logs an event to the Storybook Actions panel. + */ +export function logEvent(name: string, event: Event) { + const eventData: Record = {}; + for (const key in event) { + try { + const value = event[key as keyof Event]; + eventData[key] = value; + } catch { + // Skip properties that throw errors when accessed + } + } + action(name)(eventData); +} + +// --------------------------------------------------------------------------- +// Main API +// --------------------------------------------------------------------------- + +let userOptions: StorybookHelpersOptions = (globalThis as any)?.__WC_STORYBOOK_HELPERS_CONFIG__ || {}; + +const defaultOptions: StorybookHelpersOptions = { + typeRef: 'parsedType', + categoryOrder: ['attributes', 'properties', 'slots', 'cssProps', 'cssParts', 'cssStates', 'methods', 'events'], +}; + +/** + * Sets the global config for the Storybook helpers. + */ +export function setStorybookHelpersConfig(options: StorybookHelpersOptions) { + options = { ...defaultOptions, ...options }; + (globalThis as any).__WC_STORYBOOK_HELPERS_CONFIG__ = options; + userOptions = options; +} + +/** + * Gets Storybook helpers for a given component. + * + * Returns `args`, `argTypes`, `events`, and `logEvent` β€” everything needed for Storybook + * controls. Template rendering is intentionally excluded; use your own FAST-based + * `renderComponent()` instead. + * + * @param tagName - the tag name referenced in the Custom Elements Manifest + * @param options - optional configuration + */ +export function getStorybookHelpers(tagName: string, options?: StoryOptions): StoryHelpers { + userOptions = (globalThis as any)?.__WC_STORYBOOK_HELPERS_CONFIG__ || {}; + const cem = getManifest(); + const component = getComponent(cem, tagName); + const eventNames = component?.events?.map((event: any) => event.name) || []; + const argTypes = getArgTypes(component, options?.excludeCategories || []); + + return { + args: getArgs(argTypes), + argTypes, + events: eventNames, + logEvent, + }; +} + +function getManifest(): CEMPackage { + const cem: CEMPackage = (window as any).__STORYBOOK_CUSTOM_ELEMENTS_MANIFEST__; + if (!cem) { + throw new Error( + `Custom Elements Manifest not found. Ensure setCustomElementsManifest() is called in the Storybook preview file.\nhttps://www.npmjs.com/package/wc-storybook-helpers#before-you-install`, + ); + } + return cem; +} + +function getComponent(cem: CEMPackage, tagName: string): Component | undefined { + const component = getComponentByTagName(cem, tagName); + if (!component) { + throw new Error( + `A component with the tag name "${tagName}" was not found in the Custom Elements Manifest. ` + + `If it's missing in the CEM, it's often the result of a missing "@tag" or "@tagName" tag in the component's JSDoc.\n` + + `https://custom-elements-manifest.open-wc.org/analyzer/getting-started/#supported-jsdoc`, + ); + } + return component; +} + +function getArgTypes(component?: Component, excludeCategories?: Array): ArgTypes { + const cssProps = getCssProperties(component); + const cssParts = getCssParts(component); + const slots = getSlots(component); + + const attrsAndProps = getAttributesAndProperties(component); + const events = getEvents(component); + const cssStates = getCssStates(component); + const methods = getMethods(component); + + const args: Record = { + attributes: attrsAndProps.attrArgs, + cssParts: cssParts.args, + cssProps: cssProps.args, + cssStates: cssStates.args, + events: events.args, + methods: methods.args, + properties: attrsAndProps.propArgs, + slots: slots.args, + }; + + const argTypes: ArgTypes = {}; + + // Combine all resets + Object.assign( + argTypes, + cssProps.resets, + cssParts.resets, + slots.resets, + attrsAndProps.resets, + events.resets, + cssStates.resets, + methods.resets, + ); + + userOptions.categoryOrder?.forEach(category => { + if (excludeCategories?.includes(category)) return; + Object.assign(argTypes, args[category]); + }); + + return argTypes; +} + +function getArgs(argTypes: ArgTypes): Partial & { [key: string]: any } { + const args: Partial & { [key: string]: any } = {}; + for (const [key, value] of Object.entries(argTypes)) { + if (value?.control) { + const defaultVal = getDefaultValue(value.defaultValue); + if (defaultVal !== undefined && defaultVal !== '') { + args[key as keyof T] = defaultVal; + } + } + } + return args; +} + +function getDefaultValue(value?: string | number | boolean | object) { + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return value; + } + } + return value; +} diff --git a/packages/web-components/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md index ae1de40ebd7f8..bc2e50a46bb25 100644 --- a/packages/web-components/docs/web-components.api.md +++ b/packages/web-components/docs/web-components.api.md @@ -493,7 +493,6 @@ export class BaseAvatar extends FASTElement { // @public export class BaseButton extends FASTElement { constructor(); - autofocus: boolean; // @internal clickHandler(e: Event): boolean | void; // (undocumented) @@ -532,7 +531,6 @@ export class BaseButton extends FASTElement { // @public export class BaseCheckbox extends FASTElement { - autofocus: boolean; get checked(): boolean; set checked(next: boolean); checkValidity(): boolean; @@ -773,18 +771,15 @@ export class BaseProgressBar extends FASTElement { indicator?: HTMLElement; // @internal protected indicatorChanged(): void; - // @internal max?: number; // @internal protected maxChanged(prev: number | undefined, next: number | undefined): void; - // @internal min?: number; protected minChanged(prev: number | undefined, next: number | undefined): void; // @internal protected setIndicatorWidth(): void; validationState: ProgressBarValidationState | null; validationStateChanged(prev: ProgressBarValidationState | undefined, next: ProgressBarValidationState | undefined): void; - // @internal value?: number; // @internal protected valueChanged(prev: number | undefined, next: number | undefined): void; @@ -1011,7 +1006,6 @@ export class BaseTextArea extends FASTElement { // @public export class BaseTextInput extends FASTElement { autocomplete?: string; - autofocus: boolean; // @internal changeHandler(e: InputEvent): boolean | void; checkValidity(): boolean; @@ -4602,7 +4596,7 @@ export const zIndexPriority = "var(--zIndexPriority)"; // Warnings were encountered during analysis: // -// dist/esm/accordion-item/accordion-item.d.ts:13:5 - (ae-forgotten-export) The symbol "StaticallyComposableHTML" needs to be exported by the entry point index.d.ts +// dist/esm/accordion-item/accordion-item.d.ts:11:5 - (ae-forgotten-export) The symbol "StaticallyComposableHTML" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/web-components/package.json b/packages/web-components/package.json index cdaab2491138d..e5a3f7c29206c 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -91,7 +91,7 @@ "code-style": "yarn format:check && yarn lint", "start": "yarn start-storybook -p 6006 --docs", "start-storybook": "storybook dev", - "build-storybook": "storybook build -o ./dist/storybook --docs", + "build-storybook": "yarn analyze && storybook build -o ./dist/storybook --docs", "build-storybook:docsite": "cross-env DEPLOY_PATH=/web-components/ storybook build -o ./dist/storybook --docs", "e2e": "node ./scripts/e2e.js", "e2e:local": "node ./scripts/e2e.js --ui" @@ -106,6 +106,7 @@ "@wc-toolkit/module-path-resolver": "1.0.0", "@wc-toolkit/type-parser": "1.0.3", "chromedriver": "^125.0.0", + "dedent": "^1.2.0", "rollup-plugin-fast-tagged-templates": "^1.0.2" }, "dependencies": { diff --git a/packages/web-components/src/_docs/developer/migration.mdx b/packages/web-components/src/_docs/developer/migration.mdx index 2974f179848be..3c14d1b79ccc3 100644 --- a/packages/web-components/src/_docs/developer/migration.mdx +++ b/packages/web-components/src/_docs/developer/migration.mdx @@ -22,9 +22,9 @@ Here's a snapshot of what's new and improved in v3. A high-level look at the differences between components in v2 and v3 -- πŸ†• 15 new components +- πŸ†• 14 new components - ⚠️ 5 renamed components -- ⛔️ 4 deprecated components +- ⛔️ 4 removed components - πŸ›£οΈ 13 components on our current roadmap ### Renamed Components @@ -35,7 +35,7 @@ A high-level look at the differences between components in v2 and v3 - `text-area` = `textarea` - `text-field` = `text-input` -### Deprecated Components +### Removed Components - `anchored-region` - `slider-label` = `use field, label` @@ -52,71 +52,420 @@ A high-level look at the differences between components in v2 and v3 API changes, renames, and deprecations are itemized in the table below. -| Component | V2 | V3 | Notes | -| ----------------- | --- | --- | ------------------------------------------------------------------------------ | -| accordion | βœ… | βœ… | | -| accordion-item | βœ… | βœ… | ⚠️ API Change β†’ Attributes changed to better match web platform | -| anchor | βœ… | ⚠️ | ⛔️ Renamed β†’ See \`link\`, \`anchor-button\` | -| anchor-button | | πŸ†• | | -| anchored-region | βœ… | ⚠️ | ⛔️ Deprecated | -| avatar | | πŸ†• | | -| badge | βœ… | βœ… | | -| breadcrumb | βœ… | | | -| breadcrumb-item | βœ… | | | -| button | βœ… | βœ… | | -| calendar | βœ… | | | -| card | βœ… | | 🚧 In Progress | -| checkbox | βœ… | βœ… | | -| color | βœ… | | | -| combobox | βœ… | βœ… | | -| compound-button | | πŸ†• | | -| data-grid | βœ… | | 🚧 In Progress | -| dialog | βœ… | βœ… | ⚠️ API Change β†’ Split into two components to allow for more expressive modals. | -| dialog-body | | πŸ†• | | -| divider | βœ… | βœ… | | -| drawer | | πŸ†• | | -| drawer-body | | πŸ†• | | -| dropdown | | βœ… | | -| field | | πŸ†• | | -| flipper | βœ… | | | -| horizontal-scroll | βœ… | | | -| image | | πŸ†• | | -| listbox | βœ… | βœ… | | -| listbox-option | βœ… | βœ… | | -| label | | πŸ†• | | -| link | | βœ… | (Renamed) | -| menu | βœ… | βœ… | | -| menu-button | | πŸ†• | | -| menu-item | βœ… | βœ… | | -| menu-list | | πŸ†• | | -| message-bar | | πŸ†• | | -| number-field | βœ… | | | -| popover | | βœ… | | -| progress | βœ… | πŸ”€ | ⛔️ Renamed β†’ See \`progress-bar\` | -| progress-bar | | βœ… | (Renamed) | -| progress-ring | βœ… | | | -| radio | βœ… | βœ… | | -| radio-group | βœ… | βœ… | | -| search | βœ… | | | -| select | βœ… | βœ… | 🚧 In Progress | -| skeleton | βœ… | | | -| slider | βœ… | βœ… | | -| slider-label | βœ… | ⚠️ | ⛔️ Deprecated β†’ See \`label\`, \`field\` | -| spinner | | πŸ†• | | -| switch | βœ… | βœ… | | -| tab | βœ… | βœ… | | -| tabs | βœ… | ⚠️ | ⛔️ Deprecated β†’ See \`tablist\` | -| tab-panel | βœ… | ⚠️ | ⛔️ Deprecated | -| tablist | | πŸ†• | | -| text | | πŸ†• | | -| text-area | βœ… | πŸ”€ | ⛔️ Renamed β†’ See textarea | -| textarea | | βœ… | (Renamed) | -| text-field | βœ… | πŸ”€ | ⛔️ Renamedβ†’ See text-input | -| text-input | | βœ… | (Renamed) | -| toolbar | βœ… | | | -| tooltip | βœ… | βœ… | ⚠️ API is different from React implementation | -| tree-item | βœ… | βœ… | 🚧 In Progress | -| tree-view | βœ… | βœ… | 🚧 In Progress | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentV2V3Notes
accordionβœ…βœ…
accordion-itemβœ…βœ…βš οΈ API Change β†’ Attributes changed to better match web platform
anchorβœ…β›”οΈ + Removed β†’ Use link or anchor-button +
anchor-buttonπŸ†•
anchored-regionβœ…β›”οΈRemoved
avatarπŸ†•
badgeβœ…βœ…
breadcrumbβœ…
breadcrumb-itemβœ…
buttonβœ…βœ…
calendarβœ…
cardβœ…πŸš§ In Progress
checkboxβœ…βœ…
colorβœ…
comboboxβœ…βœ…
compound-buttonβœ…
data-gridβœ…πŸš§ In Progress
dialogβœ…βœ… + ⚠️ API Change β†’ Use with dialog-body. +
dialog-bodyπŸ†•
dividerβœ…βœ…
drawerπŸ†•
drawer-bodyπŸ†•
dropdownβœ…
fieldπŸ†•
flipperβœ…
horizontal-scrollβœ…
imageπŸ†•
listboxβœ…βœ…
listbox-optionβœ…βœ…
labelπŸ†•
linkβœ…(Renamed)
menuβœ…βœ…
menu-buttonπŸ†•
menu-itemβœ…βœ…
menu-listπŸ†•
message-barπŸ†•
number-fieldβœ…
popoverβœ…
progressβœ…πŸ”€ + Renamed β†’ See progress-bar +
progress-barπŸ”€ + Previously named progress +
progress-ringβœ…
radioβœ…βœ…
radio-groupβœ…βœ…
searchβœ…
selectβœ…πŸ”€ + Renamed β†’ Use dropdown with type="select" +
skeletonβœ…
sliderβœ…βœ…
slider-labelβœ…β›”οΈ + Removed β†’ Use label with field +
spinnerπŸ†•
switchβœ…βœ…
tabβœ…βœ…
tabsβœ…β›”οΈ + Removed β†’ Use tablist +
tab-panelβœ…β›”οΈ + Removed β†’ Use role="tabpanel" with tablist +
tablistπŸ†•
textπŸ†•
text-areaβœ…πŸ”€ + Renamed β†’ See textarea +
textareaπŸ”€ + Previously named text-area +
text-fieldβœ…πŸ”€ + Renamedβ†’ See text-input +
text-inputπŸ”€ + Previously named text-field +
toolbarβœ…
tooltipβœ…βœ…βš οΈ API differs from React implementation
tree-itemβœ…βœ…
tree-viewβœ…βœ…
### Notable API Changes diff --git a/packages/web-components/src/accordion-item/accordion-item.stories.ts b/packages/web-components/src/accordion-item/accordion-item.stories.ts index c8a88557a70df..3e9172c3b7275 100644 --- a/packages/web-components/src/accordion-item/accordion-item.stories.ts +++ b/packages/web-components/src/accordion-item/accordion-item.stories.ts @@ -1,9 +1,11 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { AccordionItem as FluentAccordionItem } from './accordion-item.js'; import { AccordionItemMarkerPosition, AccordionItemSize } from './accordion-item.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-accordion-item'); const storyTemplate = html>` html`Accordion Item`, slottedContent: () => 'Content', }, - argTypes: { - disabled: { - control: 'boolean', - description: 'Disables the accordion item.', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - expanded: { - control: 'boolean', - description: 'Expands or collapses the item.', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - headinglevel: { - control: { type: 'number', min: 1, max: 6 }, - name: 'heading-level', - description: 'Configures the level of the heading element.', - table: { - category: 'attributes', - defaultValue: { summary: '2' }, - }, - }, - size: { - control: 'select', - description: 'Controls the size of the Accordion Item.', - name: 'size', - options: ['', ...Object.values(AccordionItemSize)], - mapping: { '': null, ...AccordionItemSize }, - table: { - category: 'attributes', - type: { summary: Object.values(AccordionItemSize).join('|') }, - }, - }, - markerPosition: { - control: 'select', - description: 'Controls the position of the marker.', - name: 'marker-position', - options: ['', ...Object.values(AccordionItemMarkerPosition)], - mapping: { '': null, ...AccordionItemMarkerPosition }, - table: { - category: 'attributes', - type: { summary: Object.values(AccordionItemMarkerPosition).join('|') }, - }, - }, - block: { - control: 'boolean', - description: 'Sets the width of the focus state.', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - headingSlottedContent: { - control: false, - description: 'The content to be placed in the heading.', - name: 'heading', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = { diff --git a/packages/web-components/src/accordion-item/accordion-item.ts b/packages/web-components/src/accordion-item/accordion-item.ts index 0299b5fc89731..53e16e2b48187 100644 --- a/packages/web-components/src/accordion-item/accordion-item.ts +++ b/packages/web-components/src/accordion-item/accordion-item.ts @@ -8,8 +8,6 @@ import { AccordionItemMarkerPosition, AccordionItemSize } from './accordion-item /** * Accordion Item configuration options * - * @tag fluent-accordion-item - * * @public */ export type AccordionItemOptions = StartEndOptions & { @@ -21,6 +19,8 @@ export type AccordionItemOptions = StartEndOptions & { * An Accordion Item Custom HTML Element. * Based on BaseAccordionItem and includes style and layout specific attributes * + * @tag fluent-accordion-item + * * @public */ export class AccordionItem extends BaseAccordionItem { diff --git a/packages/web-components/src/accordion/accordion.stories.ts b/packages/web-components/src/accordion/accordion.stories.ts index 5534dec3817e6..397c77739ea27 100644 --- a/packages/web-components/src/accordion/accordion.stories.ts +++ b/packages/web-components/src/accordion/accordion.stories.ts @@ -1,9 +1,11 @@ import { html, repeat } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Accordion as FluentAccordion } from './accordion.js'; import { AccordionExpandMode } from './accordion.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-accordion'); const storyTemplate = html>` @@ -37,31 +39,7 @@ const storyTemplate = html>` export default { title: 'Components/Accordion/Accordion', render: renderComponent(storyTemplate), - argTypes: { - expandmode: { - control: 'select', - description: 'Controls the expand mode of the Accordion, either allowing single or multiple item expansion.', - name: 'expand-mode', - options: ['', ...Object.values(AccordionExpandMode)], - table: { - category: 'attributes', - defaultValue: { summary: AccordionExpandMode.multi }, - type: { summary: Object.values(AccordionExpandMode).join('|') }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - headingSlottedContent: { - control: false, - description: 'The slot for the heading content', - name: 'heading', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/accordion/accordion.ts b/packages/web-components/src/accordion/accordion.ts index 23bc871bbb5b8..eb6e6f99d8ae2 100644 --- a/packages/web-components/src/accordion/accordion.ts +++ b/packages/web-components/src/accordion/accordion.ts @@ -11,7 +11,7 @@ import { AccordionExpandMode } from './accordion.options.js'; * @tag fluent-accordion * * @slot - The default slot for the accordion items - * @fires change - Fires a custom 'change' event when the active item changes + * @fires { Event } change - Fires a custom 'change' event when the active item changes * * @public */ diff --git a/packages/web-components/src/anchor-button/anchor-button.stories.ts b/packages/web-components/src/anchor-button/anchor-button.stories.ts index 58a05b08ed6c5..00738c1098985 100644 --- a/packages/web-components/src/anchor-button/anchor-button.stories.ts +++ b/packages/web-components/src/anchor-button/anchor-button.stories.ts @@ -1,9 +1,11 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { AnchorButton as FluentAnchorButton } from './anchor-button.js'; import { AnchorButtonAppearance, AnchorButtonShape, AnchorButtonSize, AnchorTarget } from './anchor-button.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-anchor-button'); const storyTemplate = html>` 'Anchor', }, - argTypes: { - appearance: { - control: 'select', - description: 'Indicates the styled appearance of the button.', - options: ['', ...Object.values(AnchorButtonAppearance)], - mapping: { '': null, ...AnchorButtonAppearance }, - table: { - category: 'attributes', - type: { summary: Object.values(AnchorButtonAppearance).join('|') }, - }, - }, - shape: { - control: 'select', - description: 'Indicates the shape of the button.', - options: ['', ...Object.values(AnchorButtonShape)], - mapping: { '': null, ...AnchorButtonShape }, - table: { - category: 'attributes', - type: { summary: Object.values(AnchorButtonShape).join('|') }, - }, - }, - href: { - control: 'text', - description: 'The href of the anchor.', - name: 'href', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - hreflang: { - control: 'text', - description: 'Hints at the language of the referenced resource.', - name: 'hreflang', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - referrerpolicy: { - control: 'text', - description: 'The referrerpolicy attribute.', - name: 'referrerpolicy', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - rel: { - control: 'text', - description: 'The rel attribute.', - name: 'rel', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - type: { - control: 'text', - description: 'The type attribute.', - name: 'type', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - target: { - control: 'select', - description: 'The target attribute.', - options: ['', ...Object.values(AnchorTarget)], - mapping: { '': null, ...AnchorTarget }, - table: { - category: 'attributes', - type: { summary: Object.values(AnchorTarget).join('|') }, - }, - }, - iconOnly: { - control: 'boolean', - description: 'Indicates the button should only display as an icon.', - name: 'icon-only', - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - name: { - control: 'text', - description: - "The name of the element. This element's value will be surfaced during form submission under the provided name.", - table: { category: 'attributes', type: { summary: 'string' } }, - }, - size: { - control: 'select', - description: 'The size of the button.', - options: ['', ...Object.values(AnchorButtonSize)], - mapping: { '': null, ...AnchorButtonSize }, - table: { - category: 'attributes', - type: { summary: Object.values(AnchorButtonSize).join('|') }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - startSlottedContent: { - control: false, - description: 'Slot for start icons', - name: 'start', - table: { category: 'slots', type: {} }, - }, - endSlottedContent: { - control: false, - description: 'Slot for end icons', - name: 'end', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/anchor-button/anchor-button.ts b/packages/web-components/src/anchor-button/anchor-button.ts index 4273d204cbb63..cf2960f964b87 100644 --- a/packages/web-components/src/anchor-button/anchor-button.ts +++ b/packages/web-components/src/anchor-button/anchor-button.ts @@ -8,8 +8,6 @@ import { AnchorButtonAppearance, AnchorButtonShape, AnchorButtonSize } from './a /** * Anchor configuration options * - * @tag fluent-anchor-button - * * @public */ export type AnchorOptions = StartEndOptions; @@ -18,6 +16,8 @@ export type AnchorOptions = StartEndOptions; * An Anchor Custom HTML Element. * Based on BaseAnchor and includes style and layout specific attributes * + * @tag fluent-anchor-button + * * @public */ export class AnchorButton extends BaseAnchor { diff --git a/packages/web-components/src/avatar/avatar.stories.ts b/packages/web-components/src/avatar/avatar.stories.ts index 20f842e677c4b..e0ef7cfbbfd97 100644 --- a/packages/web-components/src/avatar/avatar.stories.ts +++ b/packages/web-components/src/avatar/avatar.stories.ts @@ -1,9 +1,11 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Avatar as FluentAvatar } from './avatar.js'; import { AvatarActive, AvatarAppearance, AvatarColor, AvatarShape, AvatarSize } from './avatar.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-avatar'); const storyTemplate = html>` >` export default { title: 'Components/Avatar', render: renderComponent(storyTemplate), - argTypes: { - active: { - control: 'select', - description: 'Optional activity indicator', - options: ['', ...Object.values(AvatarActive)], - mapping: { '': null, ...AvatarActive }, - table: { - category: 'attributes', - type: { summary: Object.values(AvatarActive).join('|') }, - }, - }, - appearance: { - control: 'select', - description: 'Indicates the styled appearance of the avatar.', - options: ['', ...Object.values(AvatarAppearance)], - mapping: { '': null, ...AvatarAppearance }, - table: { - category: 'attributes', - type: { summary: Object.values(AvatarAppearance).join('|') }, - }, - }, - color: { - control: 'select', - description: 'Indicates the color of the avatar.', - options: ['', ...Object.values(AvatarColor)], - mapping: { '': null, ...AvatarColor }, - table: { - category: 'attributes', - type: { summary: Object.values(AvatarColor).join('|') }, - }, - }, - initials: { - control: 'text', - description: 'Provide custom initials rather than one generated via the name', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - name: { - control: 'text', - description: - 'The name of the person or entity represented by this Avatar. This should always be provided if it is available.', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - shape: { - control: 'select', - description: 'Indicates the shape of the avatar.', - options: ['', ...Object.values(AvatarShape)], - mapping: { '': null, ...AvatarShape }, - table: { - category: 'attributes', - type: { summary: Object.values(AvatarShape).join('|') }, - }, - }, - size: { - control: 'select', - description: 'Indicates the size of the avatar.', - options: ['', ...Object.values(AvatarSize)], - mapping: { '': null, ...AvatarSize }, - table: { - category: 'attributes', - type: { summary: Object.values(AvatarSize).join('|') }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot (usually for images or icons)', - name: '', - table: { category: 'slots', type: {} }, - }, - badgeSlottedContent: { - control: false, - description: 'Badges for the avatar', - name: 'badge', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/avatar/avatar.ts b/packages/web-components/src/avatar/avatar.ts index d9f7334295ce7..6277b5c7122bf 100644 --- a/packages/web-components/src/avatar/avatar.ts +++ b/packages/web-components/src/avatar/avatar.ts @@ -16,6 +16,8 @@ import { * * @tag fluent-avatar * + * @slot badge - Optional badge content displayed with the avatar. + * * @public */ export class Avatar extends BaseAvatar { @@ -176,7 +178,7 @@ const getHashCode = (str: string): number => { for (let len: number = str.length - 1; len >= 0; len--) { const ch = str.charCodeAt(len); const shift = len % 8; - hashCode ^= (ch << shift) + (ch >> (8 - shift)); // eslint-disable-line no-bitwise + hashCode ^= (ch << shift) + (ch >> (8 - shift)); } return hashCode; diff --git a/packages/web-components/src/badge/badge.stories.ts b/packages/web-components/src/badge/badge.stories.ts index f9a1a27b7c4ba..2b32dffb6686e 100644 --- a/packages/web-components/src/badge/badge.stories.ts +++ b/packages/web-components/src/badge/badge.stories.ts @@ -1,9 +1,11 @@ import { html, when } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Badge as FluentBadge } from './badge.js'; import { BadgeAppearance, BadgeColor, BadgeShape, BadgeSize } from './badge.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-badge'); const storyTemplate = html>` 'Badge', }, - argTypes: { - appearance: { - description: 'Sets the appearance of the badge to one of the predefined styles', - options: Object.values(BadgeAppearance), - mapping: { '': null, ...BadgeAppearance }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(BadgeAppearance).join('|') }, - }, - }, - color: { - description: 'Sets the color of the badge to one of the predefined colors', - options: Object.values(BadgeColor), - mapping: { '': null, ...BadgeColor }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(BadgeColor).join('|') }, - }, - }, - shape: { - description: 'Sets the shape of the badge to one of the predefined shapes', - options: Object.values(BadgeShape), - mapping: { '': null, ...BadgeShape }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(BadgeShape).join('|') }, - }, - }, - size: { - description: 'Sets the size of the badge to one of the predefined sizes', - options: Object.values(BadgeSize), - mapping: { '': null, ...BadgeSize }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(BadgeSize).join('|') }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - startSlottedContent: { - control: false, - description: 'Slot for start icons', - name: 'start', - table: { category: 'slots', type: {} }, - }, - endSlottedContent: { - control: false, - description: 'Slot for end icons', - name: 'end', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/badge/badge.ts b/packages/web-components/src/badge/badge.ts index 0b20fd26192f4..2d3a209499846 100644 --- a/packages/web-components/src/badge/badge.ts +++ b/packages/web-components/src/badge/badge.ts @@ -5,13 +5,18 @@ import { BadgeAppearance, BadgeColor, BadgeShape, BadgeSize } from './badge.opti /** * The base class used for constructing a fluent-badge custom element + * @tag fluent-badge + * + * @slot - Content which can be provided inside the badge. + * @slot start - Content which can be provided before the badge content. + * @slot end - Content which can be provided after the badge content. + * * @public */ export class Badge extends FASTElement { /** * The appearance the badge should have. * - * @tag fluent-badge * * @public * @remarks diff --git a/packages/web-components/src/button/button.base.ts b/packages/web-components/src/button/button.base.ts index 510c6ba31ec3f..e42c2a858fedc 100644 --- a/packages/web-components/src/button/button.base.ts +++ b/packages/web-components/src/button/button.base.ts @@ -13,17 +13,6 @@ import { type ButtonFormTarget, ButtonType } from './button.options.js'; * @public */ export class BaseButton extends FASTElement { - /** - * Indicates the button should be focused when the page is loaded. - * @see The {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#autofocus | `autofocus`} attribute - * - * @public - * @remarks - * HTML Attribute: `autofocus` - */ - @attr({ mode: 'boolean' }) - public autofocus!: boolean; - /** * Default slotted content. * diff --git a/packages/web-components/src/button/button.stories.ts b/packages/web-components/src/button/button.stories.ts index 9b18ae1a84852..0da2a59f22997 100644 --- a/packages/web-components/src/button/button.stories.ts +++ b/packages/web-components/src/button/button.stories.ts @@ -1,13 +1,14 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Button as FluentButton } from './button.js'; import { ButtonAppearance, ButtonShape, ButtonSize, ButtonType } from './button.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-button'); const storyTemplate = html>` 'Button', }, - argTypes: { - appearance: { - control: 'select', - description: 'Indicates the styled appearance of the button.', - options: ['', ...Object.values(ButtonAppearance)], - mapping: { '': null, ...ButtonAppearance }, - table: { - category: 'attributes', - type: { summary: Object.values(ButtonAppearance).join('|') }, - }, - }, - disabled: { - control: 'boolean', - description: "Sets the button's disabled state.", - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - disabledFocusable: { - control: 'boolean', - description: 'Indicates the button is focusable while disabled.', - name: 'disabled-focusable', - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - formAction: { - control: 'text', - description: 'The URL that processes the form submission.', - name: 'formaction', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - formAttribute: { - control: 'text', - description: 'The id of a form to associate the element to.', - name: 'form', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - formEnctype: { - control: 'text', - description: 'The encoding type for the form submission.', - name: 'formenctype', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - formMethod: { - control: 'text', - description: 'The HTTP method that the browser uses to submit the form.', - name: 'formmethod', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - formNoValidate: { - control: 'boolean', - description: 'Indicates that the form will not be validated when submitted.', - name: 'formnovalidate', - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - formTarget: { - control: 'text', - description: 'The target frame or window to open the form submission in.', - name: 'formtarget', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - iconOnly: { - control: 'boolean', - description: 'Indicates the button should only display as an icon.', - name: 'icon-only', - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - name: { - control: 'text', - description: - "The name of the element. This element's value will be surfaced during form submission under the provided name.", - table: { category: 'attributes', type: { summary: 'string' } }, - }, - size: { - control: 'select', - description: 'The size of the button.', - options: ['', ...Object.values(ButtonSize)], - mapping: { '': null, ...ButtonSize }, - table: { - category: 'attributes', - type: { summary: Object.values(ButtonSize).join('|') }, - }, - }, - shape: { - control: 'select', - description: 'The shape of the button.', - options: ['', ...Object.values(ButtonShape)], - mapping: { '': null, ...ButtonShape }, - table: { - category: 'attributes', - type: { summary: Object.values(ButtonShape).join('|') }, - }, - }, - type: { - control: 'select', - description: 'The type of the button.', - options: ['', ...Object.values(ButtonType)], - mapping: { '': null, ...ButtonType }, - table: { - category: 'attributes', - type: { summary: Object.values(ButtonType).join('|') }, - }, - }, - value: { - control: 'text', - description: 'The initial value of the button.', - table: { category: 'attributes', type: { summary: 'string' } }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - startSlottedContent: { - control: false, - description: 'Slot for start icons', - name: 'start', - table: { category: 'slots', type: {} }, - }, - endSlottedContent: { - control: false, - description: 'Slot for end icons', - name: 'end', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/checkbox/checkbox.base.ts b/packages/web-components/src/checkbox/checkbox.base.ts index b378112216ff9..77cbb38abef38 100644 --- a/packages/web-components/src/checkbox/checkbox.base.ts +++ b/packages/web-components/src/checkbox/checkbox.base.ts @@ -4,20 +4,12 @@ import { toggleState } from '../utils/element-internals.js'; /** * The base class for a component with a toggleable checked state. * + * @fires { Event } change - Fires a custom 'change' event when the checked state changes + * @fires { Event } input - Fires a custom 'input' event when the checked state changes + * * @public */ export class BaseCheckbox extends FASTElement { - /** - * Indicates that the element should get focus after the page finishes loading. - * @see The {@link https://developer.mozilla.org/docs/Web/HTML/Element/input#autofocus | `autofocus`} attribute - * - * @public - * @remarks - * HTML Attribute: `autofocus` - */ - @attr({ mode: 'boolean' }) - public autofocus!: boolean; - /** * The element's current checked state. * diff --git a/packages/web-components/src/checkbox/checkbox.spec.ts b/packages/web-components/src/checkbox/checkbox.spec.ts index 26fbbf93209be..86c0dffb92c5e 100644 --- a/packages/web-components/src/checkbox/checkbox.spec.ts +++ b/packages/web-components/src/checkbox/checkbox.spec.ts @@ -1,3 +1,4 @@ +import { type InitialTemplateAttributes } from '@microsoft/fast-test-harness'; import { expect, test } from '../../test/playwright/index.js'; import type { Checkbox } from './checkbox.js'; @@ -419,4 +420,20 @@ test.describe('Checkbox', () => { await expect(element).toHaveJSProperty('checked', false); }); + + test('should focus the element when the `autofocus` attribute is present', async ({ fastPage, ssr }) => { + const { element } = fastPage; + + const attributes: InitialTemplateAttributes = { autofocus: true }; + + if (ssr) { + // the host element needs to be focusable for autofocus to work on the server, + // so we need to set tabindex="0" + attributes.tabindex = '0'; + } + + await fastPage.setTemplate({ attributes }); + + await expect(element).toBeFocused(); + }); }); diff --git a/packages/web-components/src/checkbox/checkbox.stories.ts b/packages/web-components/src/checkbox/checkbox.stories.ts index bb6e088483c77..2f92188787c8f 100644 --- a/packages/web-components/src/checkbox/checkbox.stories.ts +++ b/packages/web-components/src/checkbox/checkbox.stories.ts @@ -2,10 +2,12 @@ import { html, ref, repeat } from '@microsoft/fast-element'; import { uniqueId } from '@microsoft/fast-web-utilities'; import { LabelPosition } from '../field/field.options.js'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Checkbox as FluentCheckbox } from './checkbox.js'; import { CheckboxShape, CheckboxSize } from './checkbox.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-checkbox'); const storyTemplate = html>` ; export const Default: Story = {}; diff --git a/packages/web-components/src/checkbox/checkbox.ts b/packages/web-components/src/checkbox/checkbox.ts index 81e29d253f2df..4936f229710f1 100644 --- a/packages/web-components/src/checkbox/checkbox.ts +++ b/packages/web-components/src/checkbox/checkbox.ts @@ -11,8 +11,8 @@ import { CheckboxShape, CheckboxSize } from './checkbox.options.js'; * * @slot checked-indicator - The checked indicator * @slot indeterminate-indicator - The indeterminate indicator - * @fires change - Emits a custom change event when the checked state changes - * @fires input - Emits a custom input event when the checked state changes + * @fires { Event } change - Emits a custom change event when the checked state changes + * @fires { Event } input - Emits a custom input event when the checked state changes * * @public */ diff --git a/packages/web-components/src/combobox/combobox.stories.ts b/packages/web-components/src/combobox/combobox.stories.ts index a079f4c1e158f..e79f9eb5f6d55 100644 --- a/packages/web-components/src/combobox/combobox.stories.ts +++ b/packages/web-components/src/combobox/combobox.stories.ts @@ -1,11 +1,14 @@ import { html, repeat } from '@microsoft/fast-element'; +import dedent from 'dedent'; import type { Meta, StoryArgs, StoryObj } from '../helpers.stories.js'; import { renderComponent } from '../helpers.stories.js'; import type { DropdownOption as FluentDropdownOption } from '../option/option.js'; import type { Dropdown as FluentDropdown } from '../dropdown/dropdown.js'; import { DropdownAppearance, DropdownSize, DropdownType } from '../dropdown/dropdown.options.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-dropdown'); const optionTemplate = html>` >` > `; +// The Combobox component is a variant of the Dropdown component.
+// To use a combobox, use \`\` + export default { title: 'Components/Combobox', parameters: { docs: { description: { - component: `The Combobox component is a variant of the Dropdown component. - To use a combobox, use <fluent-dropdown type="combobox">.`, + component: dedent`hello + world + `, }, }, }, @@ -53,30 +60,7 @@ export default { type: DropdownType.combobox, }, argTypes: { - appearance: { - control: 'select', - options: ['', ...Object.values(DropdownAppearance)], - table: { category: 'attributes' }, - }, - type: { - control: 'radio', - options: Object.values(DropdownType), - table: { category: 'attributes' }, - }, - placeholder: { - control: 'text', - table: { category: 'attributes' }, - }, - multiple: { - control: 'boolean', - table: { category: 'attributes' }, - }, - size: { - control: 'select', - options: ['', ...Object.values(DropdownSize)], - table: { category: 'attributes' }, - }, - slottedContent: { table: { disable: true } }, + ...argTypes, slot: { table: { disable: true } }, }, } as Meta; diff --git a/packages/web-components/src/compound-button/compound-button.stories.ts b/packages/web-components/src/compound-button/compound-button.stories.ts index c45f87c77a819..1f498520ba739 100644 --- a/packages/web-components/src/compound-button/compound-button.stories.ts +++ b/packages/web-components/src/compound-button/compound-button.stories.ts @@ -1,9 +1,11 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { CompoundButton as FluentCompoundButton } from './compound-button.js'; import { CompoundButtonAppearance, CompoundButtonShape, CompoundButtonSize } from './compound-button.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-compound-button'); const storyTemplate = html>` 'Button', descriptionSlottedContent: () => html`Secondary content`, }, - argTypes: { - appearance: { - control: 'select', - description: 'Indicates the styled appearance of the button.', - options: ['', ...Object.values(CompoundButtonAppearance)], - mapping: { '': null, ...CompoundButtonAppearance }, - table: { - category: 'attributes', - type: { summary: Object.values(CompoundButtonAppearance).join('|') }, - }, - }, - shape: { - control: 'select', - description: 'The shape of the button.', - options: ['', ...Object.values(CompoundButtonShape)], - mapping: { '': null, ...CompoundButtonShape }, - table: { - category: 'attributes', - type: { summary: Object.values(CompoundButtonShape).join('|') }, - }, - }, - size: { - control: 'select', - description: 'The size of the button.', - options: ['', ...Object.values(CompoundButtonSize)], - mapping: { '': null, ...CompoundButtonSize }, - table: { - category: 'attributes', - type: { summary: Object.values(CompoundButtonSize).join('|') }, - }, - }, - disabled: { - control: 'boolean', - description: "Sets the button's disabled state.", - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - disabledFocusable: { - control: 'boolean', - description: 'Indicates the button is focusable while disabled.', - name: 'disabled-focusable', - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - descriptionSlottedContent: { - control: false, - description: 'The description slot', - name: '', - table: { category: 'slots', type: {} }, - }, - startSlottedContent: { - control: false, - description: 'Slot for start icons', - name: 'start', - table: { category: 'slots', type: {} }, - }, - endSlottedContent: { - control: false, - description: 'Slot for end icons', - name: 'end', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Appearance: Story = { diff --git a/packages/web-components/src/compound-button/compound-button.ts b/packages/web-components/src/compound-button/compound-button.ts index b693619692212..5a0b843463bfe 100644 --- a/packages/web-components/src/compound-button/compound-button.ts +++ b/packages/web-components/src/compound-button/compound-button.ts @@ -5,6 +5,9 @@ import { Button } from '../button/button.js'; * * @tag fluent-compound-button * + * @slot - The default slot for the main content of the compound button + * @slot description - The description of the compound button, shown below the main content + * * @public */ export class CompoundButton extends Button {} diff --git a/packages/web-components/src/counter-badge/counter-badge.stories.ts b/packages/web-components/src/counter-badge/counter-badge.stories.ts index 136ce5892d316..0621fac4e0b0f 100644 --- a/packages/web-components/src/counter-badge/counter-badge.stories.ts +++ b/packages/web-components/src/counter-badge/counter-badge.stories.ts @@ -1,5 +1,6 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { CounterBadge as FluentCounterBadge } from './counter-badge.js'; import { CounterBadgeAppearance, @@ -9,6 +10,7 @@ import { } from './counter-badge.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-counter-badge'); const storyTemplate = html>` >` export default { title: 'Components/Badge/Counter Badge', render: renderComponent(storyTemplate), - argTypes: { - appearance: { - description: 'Sets the appearance of the badge to one of the predefined styles', - options: ['', ...Object.values(CounterBadgeAppearance)], - mapping: { '': null, ...CounterBadgeAppearance }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(CounterBadgeAppearance).join('|') }, - }, - }, - color: { - description: 'Sets the color of the badge to one of the predefined colors', - options: ['', ...Object.values(CounterBadgeColor)], - mapping: { '': null, ...CounterBadgeColor }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(CounterBadgeColor).join('|') }, - }, - }, - shape: { - description: 'Sets the shape of the badge to one of the predefined shapes', - options: ['', ...Object.values(CounterBadgeShape)], - mapping: { '': null, ...CounterBadgeShape }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(CounterBadgeShape).join('|') }, - }, - }, - size: { - description: 'Sets the size of the badge to one of the predefined sizes', - options: ['', ...Object.values(CounterBadgeSize)], - mapping: { '': null, ...CounterBadgeSize }, - control: 'select', - table: { - category: 'attributes', - type: { summary: Object.values(CounterBadgeSize).join('|') }, - }, - }, - dot: { - control: 'boolean', - description: "Sets the badge's dot state.", - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - showZero: { - control: 'boolean', - description: "Sets the badge's show-zero state.", - table: { category: 'attributes', type: { summary: 'boolean' } }, - }, - count: { - control: 'number', - description: "Sets the badge's count attribute", - name: 'formmethod', - table: { category: 'attributes', type: { summary: 'number' } }, - }, - overflowCount: { - control: 'text', - description: "Sets the badge's overflow count attribute", - name: 'formmethod', - table: { category: 'attributes', type: { summary: 'number' } }, - }, - startSlottedContent: { - control: false, - description: 'Slot for start icons', - name: 'start', - table: { category: 'slots', type: {} }, - }, - endSlottedContent: { - control: false, - description: 'Slot for end icons', - name: 'end', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = {}; diff --git a/packages/web-components/src/counter-badge/counter-badge.ts b/packages/web-components/src/counter-badge/counter-badge.ts index dca9f526a8d62..ee29ce64946da 100644 --- a/packages/web-components/src/counter-badge/counter-badge.ts +++ b/packages/web-components/src/counter-badge/counter-badge.ts @@ -15,6 +15,9 @@ import { * * @tag fluent-counter-badge * + * @slot start - Content which can be provided before the badge content. + * @slot end - Content which can be provided after the badge content. + * * @public */ export class CounterBadge extends BaseCounterBadge { diff --git a/packages/web-components/src/dialog-body/dialog-body.stories.ts b/packages/web-components/src/dialog-body/dialog-body.stories.ts index ca77d0ef12a88..3eeb778f7d959 100644 --- a/packages/web-components/src/dialog-body/dialog-body.stories.ts +++ b/packages/web-components/src/dialog-body/dialog-body.stories.ts @@ -1,8 +1,10 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { DialogBody as FluentDialogBody } from './dialog-body.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-dialog-body'); const dismissed20Regular = html>` titleActionTemplate, }, - argTypes: { - slottedContent: { - control: false, - name: '', - description: 'The default slot, for the dialog content.', - table: { category: 'slots', type: {} }, - }, - actionSlottedContent: { - control: false, - description: 'Slot for the dialog actions, such as buttons.', - name: 'action', - table: { category: 'slots', type: {} }, - }, - titleActionSlottedContent: { - control: false, - description: 'Slot for the title action elements.', - name: 'title-action', - table: { category: 'slots', type: {} }, - }, - closeSlottedContent: { - control: false, - description: 'Slot for the close element.', - name: 'close', - table: { category: 'slots', type: {} }, - }, - titleSlottedContent: { - control: false, - description: 'Slot for the title element.', - name: 'title', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default: Story = { diff --git a/packages/web-components/src/dialog-body/dialog-body.ts b/packages/web-components/src/dialog-body/dialog-body.ts index 6e81ff041ac6b..8030207142ccf 100644 --- a/packages/web-components/src/dialog-body/dialog-body.ts +++ b/packages/web-components/src/dialog-body/dialog-body.ts @@ -5,6 +5,15 @@ import { isDialog } from '../dialog/dialog.options.js'; * * @tag fluent-dialog-body * + * @slot title - Content for the dialog title. + * @slot title-action - Content for actions shown near the title. + * @slot close - Content for the close action. + * @slot action - Content for footer actions. + * @slot - Default dialog body content. + * @csspart title - The title container. + * @csspart content - The content container. + * @csspart actions - The actions container. + * * @public * @extends FASTElement */ diff --git a/packages/web-components/src/dialog/dialog.stories.ts b/packages/web-components/src/dialog/dialog.stories.ts index e9df63d8ca5b0..dc79098321329 100644 --- a/packages/web-components/src/dialog/dialog.stories.ts +++ b/packages/web-components/src/dialog/dialog.stories.ts @@ -1,11 +1,13 @@ import { css, html, ref } from '@microsoft/fast-element'; import type { DialogBody as FluentDialogBody } from '../dialog-body/dialog-body.js'; import { generateImage, type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import { definition } from './dialog.definition.js'; import type { Dialog as FluentDialog } from './dialog.js'; import { DialogType } from './dialog.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-dialog'); const dismissCircle20Regular = html` closeTemplate, }, argTypes: { - type: { - control: 'select', - description: - '`modal`: When this type of dialog is open, the rest of the page is dimmed out and cannot be interacted with. The tab sequence is kept within the dialog and moving the focus outside the dialog will imply closing it. This is the default type of the component.

`non-modal`: When a non-modal dialog is open, the rest of the page is not dimmed out and users can interact with the rest of the page. This also implies that the tab focus can move outside the dialog when it reaches the last focusable element.

`alert`: A special type of modal dialog that interrupts the users workflow to communicate an important message or ask for a decision. Unlike a typical modal dialog, the user must take an action through the options given to dismiss the dialog, and it cannot be dismissed through the dimmed background.', - mapping: { '': null, ...DialogType }, - options: ['', ...Object.values(DialogType)], - table: { - category: 'attributes', - defaultValue: { summary: DialogType.modal }, - }, - type: Object.values(DialogType).join('|'), - }, - slottedContent: { - control: false, - name: '', - description: 'Default slot for the dialog content.', - table: { category: 'slots', type: {} }, - }, - actionSlottedContent: { table: { disable: true } }, + ...argTypes, titleSlottedContent: { table: { disable: true } }, titleActionSlottedContent: { table: { disable: true } }, + actionSlottedContent: { table: { disable: true } }, + closeSlottedContent: { table: { disable: true } }, }, } as Meta; diff --git a/packages/web-components/src/dialog/dialog.ts b/packages/web-components/src/dialog/dialog.ts index 820957409fb0e..a3e29bd28da0e 100644 --- a/packages/web-components/src/dialog/dialog.ts +++ b/packages/web-components/src/dialog/dialog.ts @@ -6,6 +6,10 @@ import { DialogType } from './dialog.options.js'; * * @tag fluent-dialog * + * @fires { ToggleEvent } toggle - Event emitted after the dialog's open state changes. + * @fires { ToggleEvent } beforetoggle - Event emitted before the dialog's open state changes. + * @slot - The default slot. {@link (DialogBody:class)} element recommended. + * * @public */ export class Dialog extends FASTElement { diff --git a/packages/web-components/src/drawer/drawer.stories.ts b/packages/web-components/src/drawer/drawer.stories.ts index 5714a9acb2ddc..ffd664446fa93 100644 --- a/packages/web-components/src/drawer/drawer.stories.ts +++ b/packages/web-components/src/drawer/drawer.stories.ts @@ -1,9 +1,11 @@ import { html } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Drawer as FluentDrawer } from './drawer.js'; import { DrawerPosition, DrawerSize, DrawerType } from './drawer.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-drawer'); const dismissed20Regular = html>`; export const Default: Story = {}; diff --git a/packages/web-components/src/drawer/drawer.styles.ts b/packages/web-components/src/drawer/drawer.styles.ts index 3e61dd1c21504..0553f008e7820 100644 --- a/packages/web-components/src/drawer/drawer.styles.ts +++ b/packages/web-components/src/drawer/drawer.styles.ts @@ -14,6 +14,7 @@ import { fontWeightRegular, lineHeightBase300, shadow64, + spacingHorizontalXXL, strokeWidthThin, } from '../theme/design-tokens.js'; @@ -68,30 +69,29 @@ export const styles = css` } dialog { + background: ${colorNeutralBackground1}; + border-radius: 0; + border: ${strokeWidthThin} solid ${colorTransparentStroke}; + border-inline-end-color: ${colorTransparentStroke}; + border-inline-start-color: var(--drawer-separator, ${colorTransparentStroke}); + box-shadow: ${shadow64}; box-sizing: border-box; - z-index: var(--drawer-elevation, 1000); - font-size: ${fontSizeBase300}; - line-height: ${lineHeightBase300}; + color: ${colorNeutralForeground1}; font-family: ${fontFamilyBase}; + font-size: ${fontSizeBase300}; font-weight: ${fontWeightRegular}; - color: ${colorNeutralForeground1}; - max-width: var(--drawer-width, 592px); - max-height: 100vh; height: 100%; - margin-inline-start: 0; + line-height: ${lineHeightBase300}; margin-inline-end: auto; - border-inline-end-color: ${colorTransparentStroke}; - border-inline-start-color: var(--drawer-separator, ${colorTransparentStroke}); + margin-inline-start: 0; + max-height: 100vh; + max-width: calc(100vw - ${spacingHorizontalXXL}); outline: none; - top: 0; + padding: 0; bottom: 0; + top: 0; width: var(--drawer-width, 592px); - border-radius: 0; - padding: 0; - max-width: var(--drawer-width, 592px); - box-shadow: ${shadow64}; - border: ${strokeWidthThin} solid ${colorTransparentStroke}; - background: ${colorNeutralBackground1}; + z-index: var(--drawer-elevation, 1000); } dialog::backdrop { diff --git a/packages/web-components/src/drawer/drawer.ts b/packages/web-components/src/drawer/drawer.ts index 91c16ca6c0c75..4751d5f4c7a98 100644 --- a/packages/web-components/src/drawer/drawer.ts +++ b/packages/web-components/src/drawer/drawer.ts @@ -15,11 +15,12 @@ import { DrawerPosition, DrawerSize, DrawerType } from './drawer.options.js'; * @attr ariaLabelledby - The ID of the element that labels the drawer. * * @csspart dialog - The dialog element of the drawer. + * @cssprop --drawer-width - Sets the width of the drawer to a custom value (e.g., 300px). * * @slot - Default slot for the content of the drawer. * - * @fires toggle - Event emitted after the dialog's open state changes. - * @fires beforetoggle - Event emitted before the dialog's open state changes. + * @fires { ToggleEvent } toggle - Event emitted after the dialog's open state changes. + * @fires { ToggleEvent } beforetoggle - Event emitted before the dialog's open state changes. * * @method show - Method to show the drawer. * @method hide - Method to hide the drawer. @@ -29,8 +30,6 @@ import { DrawerPosition, DrawerSize, DrawerType } from './drawer.options.js'; * @method emitBeforeToggle - Emits an event before the dialog's open state changes. * * @summary A component that provides a drawer for displaying content in a side panel. - * - * @tag fluent-drawer */ export class Drawer extends FASTElement { /** diff --git a/packages/web-components/src/dropdown/dropdown.base.ts b/packages/web-components/src/dropdown/dropdown.base.ts index 1b4a5b905d2e4..82f683b366746 100644 --- a/packages/web-components/src/dropdown/dropdown.base.ts +++ b/packages/web-components/src/dropdown/dropdown.base.ts @@ -25,6 +25,8 @@ import { dropdownButtonTemplate, dropdownInputTemplate } from './dropdown.templa * @slot indicator - The indicator slot. * @slot control - The control slot. This slot is automatically populated and should not be manually manipulated. * + * @fires { Event } change - Fires a custom 'change' event when the selected option changes + * * @public */ export class BaseDropdown extends FASTElement { diff --git a/packages/web-components/src/dropdown/dropdown.stories.ts b/packages/web-components/src/dropdown/dropdown.stories.ts index 5ddc69a1a8742..72579db2925fe 100644 --- a/packages/web-components/src/dropdown/dropdown.stories.ts +++ b/packages/web-components/src/dropdown/dropdown.stories.ts @@ -2,10 +2,12 @@ import { html, ref, repeat } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; import type { DropdownOption as FluentOption } from '../option/option.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Dropdown as FluentDropdown } from './dropdown.js'; import { DropdownAppearance, DropdownSize, DropdownType } from './dropdown.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-dropdown'); const optionTemplate = html>` ; diff --git a/packages/web-components/src/field/field.stories.ts b/packages/web-components/src/field/field.stories.ts index dc68b3313a2a5..0f424d2a1617d 100644 --- a/packages/web-components/src/field/field.stories.ts +++ b/packages/web-components/src/field/field.stories.ts @@ -1,10 +1,12 @@ import { html, ref, repeat } from '@microsoft/fast-element'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; import { colorStatusSuccessBackground3 } from '../theme/design-tokens.js'; +import { getStorybookHelpers } from '../../.storybook/wc-toolkit-helpers.js'; import type { Field as FluentField } from './field.js'; import { LabelPosition } from './field.options.js'; type Story = StoryObj; +const { argTypes } = getStorybookHelpers('fluent-field'); const SuccessIcon = html.partial(/* html */ ` Short image description`, }, - argTypes: { - block: { - control: 'boolean', - description: - 'An image can use the argument β€˜block’ so that it’s width will expand to fill the available container space.', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - bordered: { - control: 'boolean', - description: 'Border surrounding image', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - fit: { - control: 'select', - description: 'Determines how the image will be scaled and positioned within its parent container.', - name: 'size', - mapping: { '': null, ...ImageFit }, - options: ['', ...Object.values(ImageFit)], - table: { - category: 'attributes', - type: { summary: Object.values(ImageFit).join('|') }, - }, - }, - shadow: { - control: 'boolean', - description: 'Apply an optional box shadow to further separate the image from the background.', - table: { - category: 'attributes', - defaultValue: { summary: 'false' }, - type: { summary: 'boolean' }, - }, - }, - shape: { - control: 'select', - description: 'Image Shape', - name: 'size', - mapping: { '': null, ...ImageShape }, - options: ['', ...Object.values(ImageShape)], - table: { - category: 'attributes', - type: { summary: Object.values(ImageShape).join('|') }, - }, - }, - slottedContent: { - control: false, - description: 'The default slot', - name: '', - table: { category: 'slots', type: {} }, - }, - }, + argTypes, } as Meta; export const Default = {}; diff --git a/packages/web-components/src/image/image.ts b/packages/web-components/src/image/image.ts index 08d12f79eeaa4..7ea5b21a567e4 100644 --- a/packages/web-components/src/image/image.ts +++ b/packages/web-components/src/image/image.ts @@ -6,6 +6,8 @@ import { ImageFit, ImageShape } from './image.options.js'; * * @tag fluent-image * + * @slot - The default slot. Accepts any ``, ``, `