diff --git a/CHANGELOG.md b/CHANGELOG.md index cb54ece5fe..bfb8fab868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,25 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] + +## [v0.40.4](https://github.com/stardust-ui/react/tree/v0.40.4) (2019-11-26) +[Compare changes](https://github.com/stardust-ui/react/compare/v0.40.3...v0.40.4) + +### Fixes +- Trigger `whatInput` cleanup when last `Provider` with custom `target` gets removed @silviuavram ([#2127](https://github.com/microsoft/fluent-ui-react/pull/2127)) + +### Performance +- Allow suppression of action menu positioning in `ChatMessage` @jurokapsiar ([#2126](https://github.com/microsoft/fluent-ui-react/pull/2126)) + + +## [v0.40.3](https://github.com/stardust-ui/react/tree/v0.40.3) (2019-11-08) +[Compare changes](https://github.com/stardust-ui/react/compare/v0.40.2...v0.40.3) + +### Performance +- Add rendering performance telemetry @miroslavstastny ([#2079](https://github.com/microsoft/fluent-ui-react/pull/2079)) +- Skip empty frames in mergeThemes @levithomason @miroslavstastny ([#2095](https://github.com/microsoft/fluent-ui-react/pull/2095)) +- Lazily evaluate styles and classes @levithomason @miroslavstastny ([#2097](https://github.com/microsoft/fluent-ui-react/pull/2097)) + ## [v0.40.2](https://github.com/stardust-ui/react/tree/v0.40.2) (2019-10-30) [Compare changes](https://github.com/stardust-ui/react/compare/v0.40.1...v0.40.2) diff --git a/docs/src/prototypes/customToolbar/index.tsx b/docs/src/prototypes/customToolbar/index.tsx index c6b55e15db..b582b653d9 100644 --- a/docs/src/prototypes/customToolbar/index.tsx +++ b/docs/src/prototypes/customToolbar/index.tsx @@ -7,7 +7,7 @@ import { useSelectKnob, KnobInspector, } from '@stardust-ui/docs-components' -import { Provider, Flex, themes, mergeThemes } from '@stardust-ui/react' +import { Provider, Flex, themes, mergeThemes, Telemetry } from '@stardust-ui/react' import { darkThemeOverrides } from './darkThemeOverrides' import { highContrastThemeOverrides } from './highContrastThemeOverrides' @@ -58,6 +58,9 @@ const CustomToolbarPrototype: React.FunctionComponent = () => { const [currentSlide, setCurrentSlide] = React.useState(23) const totalSlides = 34 + const [telemetryEnabled] = useBooleanKnob({ name: 'telemetryEnabled', initialValue: true }) + const telemetryRef = React.useRef() + let theme = {} if (themeName === 'teamsDark') { theme = mergeThemes(themes.teamsDark, darkThemeOverrides) @@ -65,6 +68,35 @@ const CustomToolbarPrototype: React.FunctionComponent = () => { theme = mergeThemes(themes.teamsHighContrast, darkThemeOverrides, highContrastThemeOverrides) } + React.useEffect(() => { + performance.measure('render-custom-toolbar', 'render-custom-toolbar') + const telemetry = telemetryRef.current + if (!telemetryEnabled || !telemetry) { + return + } + + telemetry.enabled = false + + const totals = _.reduce( + telemetry.performance, + (acc, next) => { + acc.count += next.count + acc.msTotal += next.msTotal + return acc + }, + { count: 0, msTotal: 0 }, + ) + + console.log(`Rendered ${totals.count} Stardust components in ${totals.msTotal} ms`) + console.table(telemetry.performance) + }) + + if (telemetryRef.current) { + telemetryRef.current.enabled = telemetryEnabled + telemetryRef.current.reset() + } + performance.mark('render-custom-toolbar') + return (
@@ -72,7 +104,7 @@ const CustomToolbarPrototype: React.FunctionComponent = () => { - + ({ name: `${firstName} ${lastName}`, onMouseEnter: () => { this.setPopupOpen(true) @@ -63,7 +63,7 @@ class AvatarEmployeeCard extends React.Component< onMouseLeave: () => { this.setPopupOpen(false) }, - }, + }), })} content={{ styles: { marginLeft: '10px' }, diff --git a/docs/src/prototypes/employeeCard/EmployeeCard.tsx b/docs/src/prototypes/employeeCard/EmployeeCard.tsx index 9ae9a0f538..eac45e8b59 100644 --- a/docs/src/prototypes/employeeCard/EmployeeCard.tsx +++ b/docs/src/prototypes/employeeCard/EmployeeCard.tsx @@ -70,10 +70,10 @@ class EmployeeCard extends React.Component, any> { )}
{Avatar.create(avatar, { - defaultProps: { + defaultProps: () => ({ size: 'largest', name: `${firstName} ${lastName}`, - }, + }), })} ) diff --git a/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx b/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx index d79b5d6211..36612758bb 100644 --- a/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx +++ b/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx @@ -17,12 +17,12 @@ class MSTeamsLogo extends React.Component { return (
{Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ variables: { color: siteVariables.colors.brand[600] }, size: 'largest', xSpacing: 'after', styles: { verticalAlign: 'middle' }, - }, + }), })} { return StardustUIDivider.create(props, { - defaultProps: { + defaultProps: () => ({ variables: { dividerColor: 'transparent' }, - }, + }), }) } diff --git a/lerna.json b/lerna.json index 9d2b484d36..e5f91b591d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "0.40.2", + "version": "0.40.4", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/code-sandbox/package.json b/packages/code-sandbox/package.json index 95934081f3..a4db2cc1b1 100644 --- a/packages/code-sandbox/package.json +++ b/packages/code-sandbox/package.json @@ -1,11 +1,11 @@ { "name": "@stardust-ui/code-sandbox", "description": "Stardust UI tools for CodeSandbox.", - "version": "0.40.2", + "version": "0.40.4", "author": "Oleksandr Fediashov ", "bugs": "https://github.com/stardust-ui/react/issues", "dependencies": { - "@stardust-ui/react": "^0.40.2" + "@stardust-ui/react": "^0.40.4" }, "files": [ "dist" diff --git a/packages/react/package.json b/packages/react/package.json index 53ff336fa3..2de0263022 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@stardust-ui/react", "description": "A themable React component library.", - "version": "0.40.2", + "version": "0.40.4", "author": "Levi Thomason ", "bugs": "https://github.com/stardust-ui/react/issues", "dependencies": { diff --git a/packages/react/src/components/Accordion/Accordion.tsx b/packages/react/src/components/Accordion/Accordion.tsx index cb1be526dc..265b0d4833 100644 --- a/packages/react/src/components/Accordion/Accordion.tsx +++ b/packages/react/src/components/Accordion/Accordion.tsx @@ -258,7 +258,7 @@ class Accordion extends AutoControlledComponent, Acco children.push( AccordionTitle.create(title, { - defaultProps: { + defaultProps: () => ({ className: Accordion.slotClassNames.title, active, index, @@ -266,19 +266,19 @@ class Accordion extends AutoControlledComponent, Acco canBeCollapsed, id: titleId, accordionContentId: contentId, - }, + }), overrideProps: this.handleTitleOverrides, render: renderPanelTitle, }), ) children.push( AccordionContent.create(content, { - defaultProps: { + defaultProps: () => ({ className: Accordion.slotClassNames.content, active, id: contentId, accordionTitleId: titleId, - }, + }), render: renderPanelContent, }), ) diff --git a/packages/react/src/components/Accordion/AccordionTitle.tsx b/packages/react/src/components/Accordion/AccordionTitle.tsx index fd02263f0a..f30011152e 100644 --- a/packages/react/src/components/Accordion/AccordionTitle.tsx +++ b/packages/react/src/components/Accordion/AccordionTitle.tsx @@ -119,9 +119,9 @@ class AccordionTitle extends UIComponent, any> { {...accessibility.attributes.content} {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.content, unhandledProps)} start={Icon.create(indicatorWithDefaults, { - defaultProps: { + defaultProps: () => ({ styles: styles.indicator, - }, + }), })} main={rtlTextContainer.createFor({ element: content })} /> diff --git a/packages/react/src/components/Alert/Alert.tsx b/packages/react/src/components/Alert/Alert.tsx index 4bcc266668..5d7c3a2532 100644 --- a/packages/react/src/components/Alert/Alert.tsx +++ b/packages/react/src/components/Alert/Alert.tsx @@ -180,18 +180,18 @@ class Alert extends AutoControlledComponent, AlertState> const bodyContent = ( <> {Text.create(header, { - defaultProps: { + defaultProps: () => ({ className: Alert.slotClassNames.header, styles: styles.header, ...accessibility.attributes.header, - }, + }), })} {Box.create(content, { - defaultProps: { + defaultProps: () => ({ className: Alert.slotClassNames.content, styles: styles.content, ...accessibility.attributes.content, - }, + }), })} ) @@ -199,38 +199,38 @@ class Alert extends AutoControlledComponent, AlertState> return ( <> {Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ className: Alert.slotClassNames.icon, styles: styles.icon, - }, + }), })} {Box.create(body, { - defaultProps: { + defaultProps: () => ({ id: this.state.bodyId, className: Alert.slotClassNames.body, ...accessibility.attributes.body, styles: styles.body, - }, + }), overrideProps: { children: bodyContent, }, })} {ButtonGroup.create(actions, { - defaultProps: { + defaultProps: () => ({ className: Alert.slotClassNames.actions, styles: styles.actions, - }, + }), })} {dismissible && Button.create(dismissAction, { - defaultProps: { + defaultProps: () => ({ iconOnly: true, text: true, className: Alert.slotClassNames.dismissAction, styles: styles.dismissAction, ...accessibility.attributes.dismissAction, - }, + }), overrideProps: this.handleDismissOverrides, })} diff --git a/packages/react/src/components/Attachment/Attachment.tsx b/packages/react/src/components/Attachment/Attachment.tsx index 73afadaa5a..0a9f54033e 100644 --- a/packages/react/src/components/Attachment/Attachment.tsx +++ b/packages/react/src/components/Attachment/Attachment.tsx @@ -91,27 +91,27 @@ class Attachment extends UIComponent> { > {icon && Icon.create(icon, { - defaultProps: { size: 'larger', styles: styles.icon }, + defaultProps: () => ({ size: 'larger', styles: styles.icon }), })} {(header || description) && (
{Text.create(header, { - defaultProps: { styles: styles.header }, + defaultProps: () => ({ styles: styles.header }), })} {Text.create(description, { - defaultProps: { styles: styles.description }, + defaultProps: () => ({ styles: styles.description }), })}
)} {action && Button.create(action, { - defaultProps: { + defaultProps: () => ({ iconOnly: true, text: true, styles: styles.action, className: Attachment.slotClassNames.action, - }, + }), })} {!_.isNil(progress) &&
} diff --git a/packages/react/src/components/Avatar/Avatar.tsx b/packages/react/src/components/Avatar/Avatar.tsx index be6364144f..766adeed6d 100644 --- a/packages/react/src/components/Avatar/Avatar.tsx +++ b/packages/react/src/components/Avatar/Avatar.tsx @@ -91,31 +91,31 @@ class Avatar extends UIComponent, any> { return ( {Image.create(image, { - defaultProps: { + defaultProps: () => ({ fluid: true, avatar: true, title: name, styles: styles.image, - }, + }), })} {!image && Label.create(label || {}, { - defaultProps: { + defaultProps: () => ({ content: getInitials(name), circular: true, title: name, styles: styles.label, - }, + }), })} {Status.create(status, { - defaultProps: { + defaultProps: () => ({ size, styles: styles.status, variables: { borderColor: variables.statusBorderColor, borderWidth: variables.statusBorderWidth, }, - }, + }), })} ) diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx index 776ae7632c..377a81913f 100644 --- a/packages/react/src/components/Button/Button.tsx +++ b/packages/react/src/components/Button/Button.tsx @@ -149,7 +149,7 @@ class Button extends UIComponent> { {!hasChildren && loading && this.renderLoader(variables, styles)} {!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)} {Box.create(!hasChildren && content, { - defaultProps: { as: 'span', styles: styles.content }, + defaultProps: () => ({ as: 'span', styles: styles.content }), })} {!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)} @@ -160,11 +160,11 @@ class Button extends UIComponent> { const { icon, iconPosition, content } = this.props return Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ styles: styles.icon, xSpacing: !content ? 'none' : iconPosition === 'after' ? 'before' : 'after', variables: variables.icon, - }, + }), }) } @@ -172,10 +172,10 @@ class Button extends UIComponent> { const { loader } = this.props return Loader.create(loader || {}, { - defaultProps: { + defaultProps: () => ({ role: undefined, styles: styles.loader, - }, + }), }) } diff --git a/packages/react/src/components/Button/ButtonGroup.tsx b/packages/react/src/components/Button/ButtonGroup.tsx index 264422dc90..fcd18ca893 100644 --- a/packages/react/src/components/Button/ButtonGroup.tsx +++ b/packages/react/src/components/Button/ButtonGroup.tsx @@ -76,10 +76,10 @@ class ButtonGroup extends UIComponent, any> { {_.map(buttons, (button, idx) => Button.create(button, { - defaultProps: { + defaultProps: () => ({ circular, styles: this.getStyleForButtonIndex(styles, idx === 0, idx === buttons.length - 1), - }, + }), }), )} diff --git a/packages/react/src/components/Chat/Chat.tsx b/packages/react/src/components/Chat/Chat.tsx index 243fe712ca..88ee58e69e 100644 --- a/packages/react/src/components/Chat/Chat.tsx +++ b/packages/react/src/components/Chat/Chat.tsx @@ -66,7 +66,9 @@ class Chat extends UIComponent, any> { {childrenExist(children) ? children : _.map(items, item => - ChatItem.create(item, { defaultProps: { className: Chat.slotClassNames.item } }), + ChatItem.create(item, { + defaultProps: () => ({ className: Chat.slotClassNames.item }), + }), )} ) diff --git a/packages/react/src/components/Chat/ChatItem.tsx b/packages/react/src/components/Chat/ChatItem.tsx index 5b56bf43ac..1ae020ea7a 100644 --- a/packages/react/src/components/Chat/ChatItem.tsx +++ b/packages/react/src/components/Chat/ChatItem.tsx @@ -91,7 +91,7 @@ class ChatItem extends UIComponent, any> { const gutterElement = gutter && Box.create(gutter, { - defaultProps: { className: ChatItem.slotClassNames.gutter, styles: styles.gutter }, + defaultProps: () => ({ className: ChatItem.slotClassNames.gutter, styles: styles.gutter }), }) const messageElement = this.setAttachedPropValueForChatMessage(styles) @@ -108,10 +108,10 @@ class ChatItem extends UIComponent, any> { setAttachedPropValueForChatMessage = styles => { const { message, attached } = this.props const messageElement = Box.create(message, { - defaultProps: { + defaultProps: () => ({ className: ChatItem.slotClassNames.message, styles: styles.message, - }, + }), }) // the element is ChatMessage diff --git a/packages/react/src/components/Chat/ChatMessage.tsx b/packages/react/src/components/Chat/ChatMessage.tsx index 0baae72e02..2cb9fec6c8 100644 --- a/packages/react/src/components/Chat/ChatMessage.tsx +++ b/packages/react/src/components/Chat/ChatMessage.tsx @@ -100,6 +100,9 @@ export interface ChatMessageProps */ onMouseEnter?: ComponentEventHandler + /** Allows suppression of action menu positioning for performance reasons */ + positionActionMenu?: boolean + /** Reaction group applied to the message. */ reactionGroup?: ShorthandValue | ShorthandCollection @@ -140,6 +143,7 @@ class ChatMessage extends UIComponent, ChatMessageS onBlur: PropTypes.func, onFocus: PropTypes.func, onMouseEnter: PropTypes.func, + positionActionMenu: PropTypes.bool, reactionGroup: PropTypes.oneOfType([ customPropTypes.collectionShorthand, customPropTypes.itemShorthand, @@ -152,6 +156,7 @@ class ChatMessage extends UIComponent, ChatMessageS accessibility: chatMessageBehavior, as: 'div', badgePosition: 'end', + positionActionMenu: true, reactionGroupPosition: 'start', } @@ -206,48 +211,53 @@ class ChatMessage extends UIComponent, ChatMessageS actionMenu: ChatMessageProps['actionMenu'], styles: ComponentSlotStylesPrepared, ) { - const { unstable_overflow: overflow } = this.props + const { unstable_overflow: overflow, positionActionMenu } = this.props const { messageNode } = this.state const actionMenuElement = Menu.create(actionMenu, { - defaultProps: { + defaultProps: () => ({ [IS_FOCUSABLE_ATTRIBUTE]: true, accessibility: menuAsToolbarBehavior, className: ChatMessage.slotClassNames.actionMenu, styles: styles.actionMenu, - }, + }), }) if (!actionMenuElement) { return actionMenuElement } - const menuRect: DOMRect = _.invoke(this.menuRef.current, 'getBoundingClientRect') || { + const menuRect: DOMRect = (positionActionMenu && + _.invoke(this.menuRef.current, 'getBoundingClientRect')) || { height: 0, } - const messageRect: DOMRect = _.invoke(messageNode, 'getBoundingClientRect') || { height: 0 } + const messageRect: DOMRect = (positionActionMenu && + _.invoke(messageNode, 'getBoundingClientRect')) || { height: 0 } return ( , ChatMessageS const childrenPropExists = childrenExist(children) const className = childrenPropExists ? cx(classes.root, classes.content) : classes.root const badgeElement = Label.create(badge, { - defaultProps: { + defaultProps: () => ({ className: ChatMessage.slotClassNames.badge, styles: styles.badge, - }, + }), }) const reactionGroupElement = Reaction.Group.create(reactionGroup, { - defaultProps: { + defaultProps: () => ({ className: ChatMessage.slotClassNames.reactionGroup, styles: styles.reactionGroup, - }, + }), }) const actionMenuElement = this.renderActionMenu(actionMenu, styles) const authorElement = Text.create(author, { - defaultProps: { + defaultProps: () => ({ size: 'small', styles: styles.author, className: ChatMessage.slotClassNames.author, - }, + }), }) const timestampElement = Text.create(timestamp, { - defaultProps: { + defaultProps: () => ({ size: 'small', styles: styles.timestamp, timestamp: true, className: ChatMessage.slotClassNames.timestamp, - }, + }), }) const messageContent = Box.create(content, { - defaultProps: { + defaultProps: () => ({ className: ChatMessage.slotClassNames.content, styles: styles.content, - }, + }), }) return ( diff --git a/packages/react/src/components/Checkbox/Checkbox.tsx b/packages/react/src/components/Checkbox/Checkbox.tsx index f0ceb051a4..5ec87305dc 100644 --- a/packages/react/src/components/Checkbox/Checkbox.tsx +++ b/packages/react/src/components/Checkbox/Checkbox.tsx @@ -142,10 +142,10 @@ class Checkbox extends AutoControlledComponent, Checkb const { label, labelPosition, icon, toggle } = this.props const labelElement = Text.create(label, { - defaultProps: { + defaultProps: () => ({ styles: styles.label, className: Checkbox.slotClassNames.label, - }, + }), }) return ( @@ -160,13 +160,13 @@ class Checkbox extends AutoControlledComponent, Checkb > {labelPosition === 'start' && labelElement} {Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ outline: toggle && !this.state.checked, size: toggle ? 'medium' : 'smaller', className: Checkbox.slotClassNames.indicator, name: toggle ? 'stardust-circle' : 'stardust-checkmark', styles: toggle ? styles.toggle : styles.checkbox, - }, + }), })} {labelPosition === 'end' && labelElement} diff --git a/packages/react/src/components/Dialog/Dialog.tsx b/packages/react/src/components/Dialog/Dialog.tsx index b8b9cee871..a2c2f3924d 100644 --- a/packages/react/src/components/Dialog/Dialog.tsx +++ b/packages/react/src/components/Dialog/Dialog.tsx @@ -261,18 +261,18 @@ class Dialog extends AutoControlledComponent, DialogStat overrideProps: this.handleCancelButtonOverrides, }) const confirmElement = Button.create(confirmButton, { - defaultProps: { + defaultProps: () => ({ primary: true, - }, + }), overrideProps: this.handleConfirmButtonOverrides, }) const dialogActions = (cancelElement || confirmElement) && ButtonGroup.create(actions, { - defaultProps: { + defaultProps: () => ({ styles: styles.actions, - }, + }), overrideProps: { content: ( @@ -294,29 +294,29 @@ class Dialog extends AutoControlledComponent, DialogStat {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.popup, unhandledProps)} > {Header.create(header, { - defaultProps: { + defaultProps: () => ({ as: 'h2', className: Dialog.slotClassNames.header, styles: styles.header, ...accessibility.attributes.header, - }, + }), })} {Button.create(headerAction, { - defaultProps: { + defaultProps: () => ({ className: Dialog.slotClassNames.headerAction, styles: styles.headerAction, text: true, iconOnly: true, ...accessibility.attributes.headerAction, - }, + }), })} {Box.create(content, { - defaultProps: { + defaultProps: () => ({ styles: styles.content, className: Dialog.slotClassNames.content, ...accessibility.attributes.content, - }, + }), })} {DialogFooter.create(footer, { @@ -355,10 +355,10 @@ class Dialog extends AutoControlledComponent, DialogStat }} > {Box.create(overlay, { - defaultProps: { + defaultProps: () => ({ className: Dialog.slotClassNames.overlay, styles: styles.overlay, - }, + }), overrideProps: { content: dialogContent }, })} diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 88a718b03d..f27bf36d37 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -491,11 +491,11 @@ class Dropdown extends AutoControlledComponent, Dropdo
{showClearIndicator ? Icon.create(clearIndicator, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.clearIndicator, styles: styles.clearIndicator, xSpacing: 'none', - }, + }), overrideProps: (predefinedProps: IconProps) => ({ onClick: (e: React.SyntheticEvent, iconProps: IconProps) => { _.invoke(predefinedProps, 'onClick', e, iconProps) @@ -504,13 +504,13 @@ class Dropdown extends AutoControlledComponent, Dropdo }), }) : Icon.create(toggleIndicator, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.toggleIndicator, name: 'chevron-down', styles: styles.toggleIndicator, outline: true, size: 'small', - }, + }), overrideProps: (predefinedProps: IconProps) => ({ onClick: (e, indicatorProps: IconProps) => { _.invoke(predefinedProps, 'onClick', e, indicatorProps) @@ -576,14 +576,14 @@ class Dropdown extends AutoControlledComponent, Dropdo return ( {Button.create(triggerButton, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.triggerButton, content, id: triggerButtonId, fluid: true, styles: styles.triggerButton, ...restTriggerButtonProps, - }, + }), overrideProps: (predefinedProps: IconProps) => ({ onClick: e => { onClick(e) @@ -626,13 +626,13 @@ class Dropdown extends AutoControlledComponent, Dropdo const noPlaceholder = searchQuery.length > 0 || (multiple && value.length > 0) return DropdownSearchInput.create(searchInput || {}, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.searchInput, placeholder: noPlaceholder ? '' : placeholder, inline, variables, inputRef: this.inputRef, - }, + }), overrideProps: this.handleSearchInputOverrides( highlightedIndex, rtl, @@ -732,7 +732,7 @@ class Dropdown extends AutoControlledComponent, Dropdo render(item, () => { const selected = value.indexOf(item) !== -1 return DropdownItem.create(item, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.item, active: highlightedIndex === index, selected, @@ -744,7 +744,7 @@ class Dropdown extends AutoControlledComponent, Dropdo !item.hasOwnProperty('key') && { key: (item as any).header, }), - }, + }), overrideProps: this.handleItemOverrides(item, index, getItemProps, selected), render: renderItem, }) @@ -755,18 +755,18 @@ class Dropdown extends AutoControlledComponent, Dropdo ...items, loading && ListItem.create(loadingMessage, { - defaultProps: { + defaultProps: () => ({ key: 'loading-message', styles: styles.loadingMessage, - }, + }), }), !loading && items.length === 0 && ListItem.create(noResultsMessage, { - defaultProps: { + defaultProps: () => ({ key: 'no-results-message', styles: styles.noResultsMessage, - }, + }), }), ] } @@ -782,7 +782,7 @@ class Dropdown extends AutoControlledComponent, Dropdo return value.map((item: DropdownItemProps, index) => // (!) an item matches DropdownItemProps DropdownSelectedItem.create(item, { - defaultProps: { + defaultProps: () => ({ className: Dropdown.slotClassNames.selectedItem, active: this.isSelectedItemActive(index), variables, @@ -790,7 +790,7 @@ class Dropdown extends AutoControlledComponent, Dropdo !item.hasOwnProperty('key') && { key: (item as any).header, }), - }, + }), overrideProps: this.handleSelectedItemOverrides(item, rtl), render: renderSelectedItem, }), diff --git a/packages/react/src/components/Dropdown/DropdownItem.tsx b/packages/react/src/components/Dropdown/DropdownItem.tsx index c9648a8894..450d8845ad 100644 --- a/packages/react/src/components/Dropdown/DropdownItem.tsx +++ b/packages/react/src/components/Dropdown/DropdownItem.tsx @@ -108,32 +108,32 @@ class DropdownItem extends UIComponent> { styles={styles.root} onClick={this.handleClick} header={Box.create(header, { - defaultProps: { + defaultProps: () => ({ className: DropdownItem.slotClassNames.header, styles: styles.header, - }, + }), })} media={Image.create(image, { - defaultProps: { + defaultProps: () => ({ avatar: true, className: DropdownItem.slotClassNames.image, styles: styles.image, - }, + }), })} content={Box.create(content, { - defaultProps: { + defaultProps: () => ({ className: DropdownItem.slotClassNames.content, styles: styles.content, - }, + }), })} endMedia={ selected && checkable && { content: Icon.create(checkableIndicator, { - defaultProps: { + defaultProps: () => ({ className: DropdownItem.slotClassNames.checkableIndicator, styles: styles.checkableIndicator, - }, + }), }), styles: styles.endMedia, } diff --git a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx index 884db0f641..1930c89203 100644 --- a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx +++ b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx @@ -133,22 +133,22 @@ class DropdownSelectedItem extends UIComponent ({ as: 'span', className: DropdownSelectedItem.slotClassNames.header, styles: styles.header, - }, + }), }) const renderIcon = _.isNil(icon) ? icon : render => render(icon, (ComponentType, props) => Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ 'aria-label': `Remove ${header} from selection.`, // TODO: Extract this in a behaviour. className: DropdownSelectedItem.slotClassNames.icon, styles: styles.icon, - }, + }), overrideProps: this.handleIconOverrides(props), }), ) @@ -157,11 +157,11 @@ class DropdownSelectedItem extends UIComponent render(image, (ComponentType, props) => Image.create(image, { - defaultProps: { + defaultProps: () => ({ avatar: true, className: DropdownSelectedItem.slotClassNames.image, styles: styles.image, - }, + }), overrideProps: props, }), ) diff --git a/packages/react/src/components/Embed/Embed.tsx b/packages/react/src/components/Embed/Embed.tsx index 8ecafa8f5d..392f953d93 100644 --- a/packages/react/src/components/Embed/Embed.tsx +++ b/packages/react/src/components/Embed/Embed.tsx @@ -167,7 +167,7 @@ class Embed extends AutoControlledComponent, EmbedState> {active && ( <> {Video.create(video, { - defaultProps: { + defaultProps: () => ({ autoPlay: true, controls: false, loop: true, @@ -178,15 +178,15 @@ class Embed extends AutoControlledComponent, EmbedState> width: variables.width, height: variables.height, }, - }, + }), })} {iframe && ( {Box.create(iframe, { - defaultProps: { + defaultProps: () => ({ as: 'iframe', styles: styles.iframe, - }, + }), overrideProps: this.handleFrameOverrides, })} @@ -197,13 +197,13 @@ class Embed extends AutoControlledComponent, EmbedState> {placeholderVisible && placeholderElement} {controlVisible && Icon.create(control, { - defaultProps: { + defaultProps: () => ({ className: Embed.slotClassNames.control, circular: true, name: active ? 'stardust-pause' : 'stardust-play', size: 'largest', styles: styles.control, - }, + }), })} ) diff --git a/packages/react/src/components/Form/Form.tsx b/packages/react/src/components/Form/Form.tsx index c7fe3bea4b..4265d0c349 100644 --- a/packages/react/src/components/Form/Form.tsx +++ b/packages/react/src/components/Form/Form.tsx @@ -99,7 +99,7 @@ class Form extends UIComponent, any> { renderFields = () => { const { fields } = this.props return _.map(fields, field => - FormField.create(field, { defaultProps: { className: Form.slotClassNames.field } }), + FormField.create(field, { defaultProps: () => ({ className: Form.slotClassNames.field }) }), ) } } diff --git a/packages/react/src/components/Form/FormField.tsx b/packages/react/src/components/Form/FormField.tsx index 4a1c3e8b8f..ad512fe0ea 100644 --- a/packages/react/src/components/Form/FormField.tsx +++ b/packages/react/src/components/Form/FormField.tsx @@ -85,21 +85,21 @@ class FormField extends UIComponent, any> { const { children, control, id, label, message, name, required, type } = this.props const labelElement = Text.create(label, { - defaultProps: { + defaultProps: () => ({ as: 'label', htmlFor: id, styles: styles.label, - }, + }), }) const messageElement = Text.create(message, { - defaultProps: { + defaultProps: () => ({ styles: styles.message, - }, + }), }) const controlElement = Box.create(control || {}, { - defaultProps: { required, id, name, type, styles: styles.control }, + defaultProps: () => ({ required, id, name, type, styles: styles.control }), }) const content = ( diff --git a/packages/react/src/components/Header/Header.tsx b/packages/react/src/components/Header/Header.tsx index 00df5b9d4f..8764f2e2eb 100644 --- a/packages/react/src/components/Header/Header.tsx +++ b/packages/react/src/components/Header/Header.tsx @@ -84,12 +84,12 @@ class Header extends UIComponent, any> { {rtlTextContainer.createFor({ element: contentElement, condition: !!description })} {!hasChildren && HeaderDescription.create(description, { - defaultProps: { + defaultProps: () => ({ className: Header.slotClassNames.description, variables: { ...(v.descriptionColor && { color: v.descriptionColor }), }, - }, + }), })} ) diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx index c42dbf08be..96d4f04610 100644 --- a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx +++ b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx @@ -173,13 +173,13 @@ class HierarchicalTree extends AutoControlledComponent< return _.map(items, (item: ShorthandValue, index: number) => HierarchicalTreeItem.create(item, { - defaultProps: { + defaultProps: () => ({ className: HierarchicalTree.slotClassNames.item, index, exclusive, renderItemTitle, open: exclusive ? index === activeIndex : _.includes(activeIndexes, index), - }, + }), overrideProps: this.handleTreeItemOverrides, }), ) diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx index 54f2421a34..47a0587af2 100644 --- a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx +++ b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx @@ -167,24 +167,24 @@ class HierarchicalTreeItem extends UIComponent {HierarchicalTreeTitle.create(title, { - defaultProps: { + defaultProps: () => ({ className: HierarchicalTreeItem.slotClassNames.title, open, hasSubtree, as: hasSubtree ? 'span' : 'a', - }, + }), render: renderItemTitle, overrideProps: this.handleTitleOverrides, })} {hasSubtree && open && ( {HierarchicalTree.create(items, { - defaultProps: { + defaultProps: () => ({ accessibility: hierarchicalSubtreeBehavior, className: HierarchicalTreeItem.slotClassNames.subtree, exclusive, renderItemTitle, - }, + }), })} )} diff --git a/packages/react/src/components/Input/Input.tsx b/packages/react/src/components/Input/Input.tsx index 654c70c2c2..227177f1c0 100644 --- a/packages/react/src/components/Input/Input.tsx +++ b/packages/react/src/components/Input/Input.tsx @@ -141,7 +141,7 @@ class Input extends AutoControlledComponent, InputState> const [htmlInputProps, restProps] = partitionHTMLProps(unhandledProps) return Box.create(wrapper, { - defaultProps: { + defaultProps: () => ({ ...accessibility.attributes.root, className: cx(Input.className, className), children: ( @@ -153,7 +153,7 @@ class Input extends AutoControlledComponent, InputState> }} > {Box.create(input || type, { - defaultProps: { + defaultProps: () => ({ ...htmlInputProps, as: 'input', type, @@ -162,21 +162,21 @@ class Input extends AutoControlledComponent, InputState> styles: styles.input, onChange: this.handleChange, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.input, htmlInputProps), - }, + }), })} {Icon.create(this.computeIcon(), { - defaultProps: { + defaultProps: () => ({ styles: styles.icon, variables: variables.icon, - }, + }), overrideProps: this.handleIconOverrides, })} ), styles: styles.root, ...restProps, - }, + }), overrideProps: { as: (wrapper && (wrapper as any).as) || ElementType, }, diff --git a/packages/react/src/components/Label/Label.tsx b/packages/react/src/components/Label/Label.tsx index 278f579e61..e56690bfc8 100644 --- a/packages/react/src/components/Label/Label.tsx +++ b/packages/react/src/components/Label/Label.tsx @@ -100,16 +100,16 @@ class Label extends UIComponent, any> { } const imageElement = Image.create(image, { - defaultProps: { + defaultProps: () => ({ styles: styles.image, variables: variables.image, - }, + }), }) const iconElement = Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ styles: styles.icon, variables: variables.icon, - }, + }), overrideProps: this.handleIconOverrides, }) diff --git a/packages/react/src/components/List/List.tsx b/packages/react/src/components/List/List.tsx index 26b6329524..cfddbb2711 100644 --- a/packages/react/src/components/List/List.tsx +++ b/packages/react/src/components/List/List.tsx @@ -161,12 +161,12 @@ class List extends AutoControlledComponent, ListState> { maybeSelectableItemProps.selected = index === selectedIndex } - const itemProps = { + const itemProps = () => ({ className: List.slotClassNames.item, ..._.pick(this.props, List.itemProps), ...maybeSelectableItemProps, index, - } + }) return ListItem.create(item, { defaultProps: itemProps, diff --git a/packages/react/src/components/List/ListItem.tsx b/packages/react/src/components/List/ListItem.tsx index 1f04b70064..ec28da6b45 100644 --- a/packages/react/src/components/List/ListItem.tsx +++ b/packages/react/src/components/List/ListItem.tsx @@ -119,40 +119,40 @@ class ListItem extends UIComponent> { const { endMedia, media, content, contentMedia, header, headerMedia } = this.props const contentElement = Box.create(content, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.content, styles: styles.content, - }, + }), }) const contentMediaElement = Box.create(contentMedia, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.contentMedia, styles: styles.contentMedia, - }, + }), }) const headerElement = Box.create(header, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.header, styles: styles.header, - }, + }), }) const headerMediaElement = Box.create(headerMedia, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.headerMedia, styles: styles.headerMedia, - }, + }), }) const endMediaElement = Box.create(endMedia, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.endMedia, styles: styles.endMedia, - }, + }), }) const mediaElement = Box.create(media, { - defaultProps: { + defaultProps: () => ({ className: ListItem.slotClassNames.media, styles: styles.media, - }, + }), }) return ( diff --git a/packages/react/src/components/Loader/Loader.tsx b/packages/react/src/components/Loader/Loader.tsx index 85bef3c89c..fc1125febe 100644 --- a/packages/react/src/components/Loader/Loader.tsx +++ b/packages/react/src/components/Loader/Loader.tsx @@ -117,7 +117,7 @@ class Loader extends UIComponent, LoaderState> { const { visible } = this.state const svgElement = Box.create(svg, { - defaultProps: { className: Loader.slotClassNames.svg, styles: styles.svg }, + defaultProps: () => ({ className: Loader.slotClassNames.svg, styles: styles.svg }), }) return ( @@ -128,14 +128,14 @@ class Loader extends UIComponent, LoaderState> { {...unhandledProps} > {Box.create(indicator, { - defaultProps: { + defaultProps: () => ({ children: svgElement, className: Loader.slotClassNames.indicator, styles: styles.indicator, - }, + }), })} {Text.create(label, { - defaultProps: { className: Loader.slotClassNames.label, styles: styles.label }, + defaultProps: () => ({ className: Loader.slotClassNames.label, styles: styles.label }), })} ) diff --git a/packages/react/src/components/Menu/Menu.tsx b/packages/react/src/components/Menu/Menu.tsx index aecaae9ec5..c66af2c734 100644 --- a/packages/react/src/components/Menu/Menu.tsx +++ b/packages/react/src/components/Menu/Menu.tsx @@ -197,7 +197,7 @@ class Menu extends AutoControlledComponent, MenuState> { if (kind === 'divider') { return MenuDivider.create(item, { - defaultProps: { + defaultProps: () => ({ className: Menu.slotClassNames.divider, primary, secondary, @@ -207,7 +207,7 @@ class Menu extends AutoControlledComponent, MenuState> { accessibility: accessibility.childBehaviors ? accessibility.childBehaviors.divider : undefined, - }, + }), overrideProps: overrideDividerProps, }) } @@ -215,7 +215,7 @@ class Menu extends AutoControlledComponent, MenuState> { itemPosition++ return MenuItem.create(item, { - defaultProps: { + defaultProps: () => ({ className: Menu.slotClassNames.item, iconOnly, pills, @@ -233,7 +233,7 @@ class Menu extends AutoControlledComponent, MenuState> { accessibility: accessibility.childBehaviors ? accessibility.childBehaviors.item : undefined, - }, + }), overrideProps: overrideItemProps, }) }) diff --git a/packages/react/src/components/Menu/MenuItem.tsx b/packages/react/src/components/Menu/MenuItem.tsx index 1b4c9425ad..9e2d9adc36 100644 --- a/packages/react/src/components/Menu/MenuItem.tsx +++ b/packages/react/src/components/Menu/MenuItem.tsx @@ -232,22 +232,21 @@ class MenuItem extends AutoControlledComponent, MenuIt {...(!wrapper && { onClick: this.handleClick })} {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)} > - {icon && - Icon.create(this.props.icon, { - defaultProps: { - xSpacing: !!content ? 'after' : 'none', - styles: styles.icon, - }, - })} + {Icon.create(icon, { + defaultProps: () => ({ + xSpacing: !!content ? 'after' : 'none', + styles: styles.icon, + }), + })} {Box.create(content, { - defaultProps: { as: 'span', styles: styles.content }, + defaultProps: () => ({ as: 'span', styles: styles.content }), })} {menu && Icon.create(indicatorWithDefaults, { - defaultProps: { + defaultProps: () => ({ name: vertical ? 'stardust-menu-arrow-end' : 'stardust-menu-arrow-down', styles: styles.indicator, - }, + }), })} @@ -262,7 +261,7 @@ class MenuItem extends AutoControlledComponent, MenuIt targetRef={this.itemRef} > {Menu.create(menu, { - defaultProps: { + defaultProps: () => ({ accessibility: submenuBehavior, className: MenuItem.slotClassNames.submenu, vertical: true, @@ -271,7 +270,7 @@ class MenuItem extends AutoControlledComponent, MenuIt styles: styles.menu, submenu: true, indicator, - }, + }), })} @@ -281,11 +280,11 @@ class MenuItem extends AutoControlledComponent, MenuIt if (wrapper) { return Box.create(wrapper, { - defaultProps: { + defaultProps: () => ({ className: cx(MenuItem.slotClassNames.wrapper, classes.wrapper), ...accessibility.attributes.wrapper, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper), - }, + }), overrideProps: () => ({ children: ( <> diff --git a/packages/react/src/components/MenuButton/MenuButton.tsx b/packages/react/src/components/MenuButton/MenuButton.tsx index e8463381d9..abc5bcb893 100644 --- a/packages/react/src/components/MenuButton/MenuButton.tsx +++ b/packages/react/src/components/MenuButton/MenuButton.tsx @@ -256,10 +256,10 @@ export default class MenuButton extends AutoControlledComponent ({ ...accessibility.attributes.menu, vertical: true, - }, + }), overrideProps: this.handleMenuOverrides, }) diff --git a/packages/react/src/components/Popup/Popup.tsx b/packages/react/src/components/Popup/Popup.tsx index 9fefa35fa8..1c293b89f3 100644 --- a/packages/react/src/components/Popup/Popup.tsx +++ b/packages/react/src/components/Popup/Popup.tsx @@ -494,7 +494,7 @@ export default class Popup extends AutoControlledComponent ({ ...(rtl && { dir: 'rtl' }), ...accessibility.attributes.popup, ...accessibility.keyHandlers.popup, @@ -505,7 +505,7 @@ export default class Popup extends AutoControlledComponent } /** @@ -76,6 +79,7 @@ class Provider extends React.Component> { disableAnimations: PropTypes.bool, children: PropTypes.node.isRequired, target: PropTypes.object, + telemetryRef: customPropTypes.ref, } static defaultProps = { @@ -89,6 +93,8 @@ class Provider extends React.Component> { outgoingContext: ProviderContextPrepared staticStylesRendered: boolean = false + telemetry: Telemetry + renderStaticStyles = (renderer: Renderer, mergedTheme: ThemePrepared) => { const { siteVariables } = mergedTheme const { staticStyles } = this.props.theme @@ -142,6 +148,12 @@ class Provider extends React.Component> { } } + componentWillUnmount() { + if (this.props.target) { + tryCleanupWhatInput(this.props.target) + } + } + render() { const { as, @@ -153,14 +165,27 @@ class Provider extends React.Component> { target, theme, variables, + telemetryRef, ...unhandledProps } = this.props + + if (telemetryRef) { + if (!this.telemetry) { + this.telemetry = new Telemetry() + } + + telemetryRef['current'] = this.telemetry + } else if (this.telemetry) { + delete this.telemetry + } + const inputContext: ProviderContextInput = { theme, rtl, disableAnimations, renderer, target, + telemetry: this.telemetry, } const incomingContext: ProviderContextPrepared = overwrite ? {} : this.context diff --git a/packages/react/src/components/RadioGroup/RadioGroup.tsx b/packages/react/src/components/RadioGroup/RadioGroup.tsx index 9fbbb01c65..8567ebee51 100644 --- a/packages/react/src/components/RadioGroup/RadioGroup.tsx +++ b/packages/react/src/components/RadioGroup/RadioGroup.tsx @@ -178,11 +178,11 @@ class RadioGroup extends AutoControlledComponent, an return _.map(items, (item, index) => RadioGroupItem.create(item, { - defaultProps: { + defaultProps: () => ({ className: RadioGroup.slotClassNames.item, vertical, ...(index === 0 && isNoneValueSelected && { tabIndex: 0 }), - }, + }), overrideProps: this.handleItemOverrides, }), ) diff --git a/packages/react/src/components/RadioGroup/RadioGroupItem.tsx b/packages/react/src/components/RadioGroup/RadioGroupItem.tsx index 6c126e0960..fdab3a174f 100644 --- a/packages/react/src/components/RadioGroup/RadioGroupItem.tsx +++ b/packages/react/src/components/RadioGroup/RadioGroupItem.tsx @@ -136,15 +136,15 @@ class RadioGroupItem extends AutoControlledComponent< {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)} > {Icon.create(icon || 'stardust-circle', { - defaultProps: { + defaultProps: () => ({ size: 'small', styles: styles.icon, - }, + }), })} {Box.create(label, { - defaultProps: { + defaultProps: () => ({ as: 'span', - }, + }), })} diff --git a/packages/react/src/components/Reaction/Reaction.tsx b/packages/react/src/components/Reaction/Reaction.tsx index fb44a1fee6..e4ee81525e 100644 --- a/packages/react/src/components/Reaction/Reaction.tsx +++ b/packages/react/src/components/Reaction/Reaction.tsx @@ -74,16 +74,16 @@ class Reaction extends UIComponent> { ) : ( <> {Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ className: Reaction.slotClassNames.icon, styles: styles.icon, - }, + }), })} {Box.create(content, { - defaultProps: { + defaultProps: () => ({ className: Reaction.slotClassNames.content, styles: styles.content, - }, + }), })} )} diff --git a/packages/react/src/components/Reaction/ReactionGroup.tsx b/packages/react/src/components/Reaction/ReactionGroup.tsx index 7c795d149f..73d9317863 100644 --- a/packages/react/src/components/Reaction/ReactionGroup.tsx +++ b/packages/react/src/components/Reaction/ReactionGroup.tsx @@ -67,9 +67,9 @@ class ReactionGroup extends UIComponent> { {_.map(items, reaction => Reaction.create(reaction, { - defaultProps: { + defaultProps: () => ({ styles: styles.reaction, - }, + }), }), )} diff --git a/packages/react/src/components/Slider/Slider.tsx b/packages/react/src/components/Slider/Slider.tsx index a0c8bd9771..ec8bdcd10c 100644 --- a/packages/react/src/components/Slider/Slider.tsx +++ b/packages/react/src/components/Slider/Slider.tsx @@ -192,7 +192,7 @@ class Slider extends AutoControlledComponent, SliderStat }} > {Box.create(input || type, { - defaultProps: { + defaultProps: () => ({ ...htmlInputProps, ...accessibility.attributes.input, className: Slider.slotClassNames.input, @@ -204,7 +204,7 @@ class Slider extends AutoControlledComponent, SliderStat value, styles: styles.input, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.input, htmlInputProps), - }, + }), overrideProps: this.handleInputOverrides, })} diff --git a/packages/react/src/components/SplitButton/SplitButton.tsx b/packages/react/src/components/SplitButton/SplitButton.tsx index e0bc5bf363..299342d58f 100644 --- a/packages/react/src/components/SplitButton/SplitButton.tsx +++ b/packages/react/src/components/SplitButton/SplitButton.tsx @@ -173,12 +173,12 @@ class SplitButton extends AutoControlledComponent, }: RenderResultConfig): React.ReactNode { const { button, disabled, menu, primary, secondary, toggleButton } = this.props const trigger = Button.create(button, { - defaultProps: { + defaultProps: () => ({ styles: styles.button, primary, secondary, disabled, - }, + }), overrideProps: this.handleMenuButtonTriggerOverrides, }) @@ -187,7 +187,7 @@ class SplitButton extends AutoControlledComponent, {MenuButton.create( {}, { - defaultProps: { + defaultProps: () => ({ accessibility: accessibility.childBehaviors ? accessibility.childBehaviors.menuButton : undefined, @@ -196,12 +196,12 @@ class SplitButton extends AutoControlledComponent, on: [], open: this.state.open, trigger, - }, + }), overrideProps: this.handleMenuButtonOverrides, }, )} {Button.create(toggleButton, { - defaultProps: { + defaultProps: () => ({ className: SplitButton.slotClassNames.toggleButton, disabled, icon: 'stardust-arrow-down', @@ -209,7 +209,7 @@ class SplitButton extends AutoControlledComponent, primary, secondary, ...accessibility.attributes.toggleButton, - }, + }), overrideProps: (predefinedProps: ButtonProps) => ({ onClick: (e: React.SyntheticEvent, buttonProps: ButtonProps) => { _.invoke(predefinedProps, 'onClick', e, buttonProps) diff --git a/packages/react/src/components/Status/Status.tsx b/packages/react/src/components/Status/Status.tsx index 50c0230e55..de7678241c 100644 --- a/packages/react/src/components/Status/Status.tsx +++ b/packages/react/src/components/Status/Status.tsx @@ -61,12 +61,12 @@ class Status extends UIComponent, any> { return ( {Icon.create(icon, { - defaultProps: { + defaultProps: () => ({ size: 'smallest', styles: styles.icon, variables: variables.icon, xSpacing: 'none', - }, + }), })} ) diff --git a/packages/react/src/components/Toolbar/Toolbar.tsx b/packages/react/src/components/Toolbar/Toolbar.tsx index aee6f85568..147fae0601 100644 --- a/packages/react/src/components/Toolbar/Toolbar.tsx +++ b/packages/react/src/components/Toolbar/Toolbar.tsx @@ -167,7 +167,7 @@ class Toolbar extends UIComponent> { return ToolbarRadioGroup.create(item, { overrideProps: itemOverridesFn }) case 'toggle': return ToolbarItem.create(item, { - defaultProps: { accessibility: toggleButtonBehavior }, + defaultProps: () => ({ accessibility: toggleButtonBehavior }), overrideProps: itemOverridesFn, }) case 'custom': @@ -471,9 +471,9 @@ class Toolbar extends UIComponent> { return ( {ToolbarItem.create(overflowItem, { - defaultProps: { + defaultProps: () => ({ icon: { name: 'more', outline: true }, - }, + }), overrideProps: { menu: this.props.overflowOpen ? this.getOverflowItems() : [], menuOpen: this.props.overflowOpen, diff --git a/packages/react/src/components/Toolbar/ToolbarItem.tsx b/packages/react/src/components/Toolbar/ToolbarItem.tsx index a0fcad6c73..88cc133046 100644 --- a/packages/react/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react/src/components/Toolbar/ToolbarItem.tsx @@ -257,9 +257,9 @@ class ToolbarItem extends UIComponent> { if (popup) { return Popup.create(popup, { - defaultProps: { + defaultProps: () => ({ trapFocus: true, - }, + }), overrideProps: { trigger: itemElement, children: undefined, // force-reset `children` defined for `Popup` as it collides with the `trigger` @@ -278,11 +278,11 @@ class ToolbarItem extends UIComponent> { if (wrapper) { return Box.create(wrapper, { - defaultProps: { + defaultProps: () => ({ className: cx(ToolbarItem.slotClassNames.wrapper, classes.wrapper), ...accessibility.attributes.wrapper, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper), - }, + }), overrideProps: predefinedProps => ({ children: contentElement, onClick: e => { diff --git a/packages/react/src/components/Toolbar/ToolbarMenu.tsx b/packages/react/src/components/Toolbar/ToolbarMenu.tsx index 6f7de3afb2..ac332cd1a4 100644 --- a/packages/react/src/components/Toolbar/ToolbarMenu.tsx +++ b/packages/react/src/components/Toolbar/ToolbarMenu.tsx @@ -120,16 +120,16 @@ class ToolbarMenu extends UIComponent { case 'toggle': return ToolbarMenuItem.create(item, { - defaultProps: { accessibility: toolbarMenuItemCheckboxBehavior }, + defaultProps: () => ({ accessibility: toolbarMenuItemCheckboxBehavior }), overrideProps: itemOverridesFn, }) default: return ToolbarMenuItem.create(item, { - defaultProps: { + defaultProps: () => ({ submenuIndicator, inSubmenu: submenu, - }, + }), overrideProps: itemOverridesFn, }) } diff --git a/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx b/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx index 46fee27110..3c498edded 100644 --- a/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx +++ b/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx @@ -300,21 +300,23 @@ class ToolbarMenuItem extends AutoControlledComponent< children ) : ( <> - {Icon.create(icon, { defaultProps: { xSpacing: !!content ? 'after' : 'none' } })} + {Icon.create(icon, { + defaultProps: () => ({ xSpacing: !!content ? 'after' : 'none' }), + })} {content} {active && Icon.create(activeIndicator, { - defaultProps: { + defaultProps: () => ({ className: ToolbarMenuItem.slotClassNames.activeIndicator, styles: styles.activeIndicator, - }, + }), })} {menu && Icon.create(submenuIndicator, { - defaultProps: { + defaultProps: () => ({ name: 'stardust-menu-arrow-end', styles: styles.submenuIndicator, - }, + }), })} )} @@ -325,12 +327,12 @@ class ToolbarMenuItem extends AutoControlledComponent< if (popup && !hasChildren) { return Popup.create(popup, { - defaultProps: { + defaultProps: () => ({ trapFocus: true, onOpenChange: e => { e.stopPropagation() }, - }, + }), overrideProps: { trigger: elementType, children: undefined, // force-reset `children` defined for `Popup` as it collides with the `trigger` @@ -353,12 +355,12 @@ class ToolbarMenuItem extends AutoControlledComponent< > {ToolbarMenu.create(menu, { - defaultProps: { + defaultProps: () => ({ className: ToolbarMenuItem.slotClassNames.submenu, styles: styles.menu, submenu: true, submenuIndicator, - }, + }), overrideProps: this.handleMenuOverrides(getRefs), })} @@ -378,11 +380,11 @@ class ToolbarMenuItem extends AutoControlledComponent< } return Box.create(wrapper, { - defaultProps: { + defaultProps: () => ({ className: cx(ToolbarMenuItem.slotClassNames.wrapper, classes.wrapper), ...accessibility.attributes.wrapper, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper), - }, + }), overrideProps: () => ({ children: ( <> diff --git a/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx b/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx index 83ddd05d6b..b773686888 100644 --- a/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx +++ b/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx @@ -108,12 +108,12 @@ class ToolbarMenuRadioGroup extends UIComponent {_.map(items, (item, index) => ToolbarMenuItem.create(item, { - defaultProps: { + defaultProps: () => ({ accessibility: toolbarMenuItemRadioBehavior, as: 'li', active: activeIndex === index, index, - }, + }), overrideProps: this.handleItemOverrides(variables), }), )} @@ -121,13 +121,13 @@ class ToolbarMenuRadioGroup extends UIComponent ({ as: 'li', className: ToolbarMenuRadioGroup.slotClassNames.wrapper, styles: styles.wrapper, ...accessibility.attributes.wrapper, ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper), - }, + }), overrideProps: { children: content, }, diff --git a/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx b/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx index c73151401c..0052cbdaba 100644 --- a/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx +++ b/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx @@ -129,10 +129,10 @@ class ToolbarRadioGroup extends UIComponent> } const toolbarItem = ToolbarItem.create(item, { - defaultProps: { + defaultProps: () => ({ accessibility: toolbarRadioGroupItemBehavior, active: activeIndex === index, - }, + }), overrideProps: itemOverridesFn, }) diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx index dc751dacd6..0e6fb6914b 100644 --- a/packages/react/src/components/Tooltip/Tooltip.tsx +++ b/packages/react/src/components/Tooltip/Tooltip.tsx @@ -283,13 +283,13 @@ export default class Tooltip extends AutoControlledComponent ({ ...tooltipContentAttributes, open: this.state.open, placement, pointing, pointerRef: this.pointerTargetRef, - }, + }), overrideProps: this.getContentProps, }) diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx index 334e4d5b44..b69980d7b9 100644 --- a/packages/react/src/components/Tree/Tree.tsx +++ b/packages/react/src/components/Tree/Tree.tsx @@ -275,7 +275,7 @@ class Tree extends AutoControlledComponent, TreeState> { const isSubtree = hasSubtree(item) const isSubtreeOpen = isSubtree && this.isActiveItem(item['id']) const renderedItem = TreeItem.create(item, { - defaultProps: { + defaultProps: () => ({ accessibility: accessibility.childBehaviors ? accessibility.childBehaviors.item : undefined, @@ -285,7 +285,7 @@ class Tree extends AutoControlledComponent, TreeState> { key: item['id'], contentRef: elementRef, ...restItemForRender, - }, + }), overrideProps: this.handleTreeItemOverrides, }) diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx index 8f0d34348d..f1d5ea1a44 100644 --- a/packages/react/src/components/Tree/TreeItem.tsx +++ b/packages/react/src/components/Tree/TreeItem.tsx @@ -196,7 +196,7 @@ class TreeItem extends UIComponent, TreeItemState> { const { hasSubtree, treeSize } = this.state return TreeTitle.create(title, { - defaultProps: { + defaultProps: () => ({ className: TreeItem.slotClassNames.title, open, hasSubtree, @@ -207,7 +207,7 @@ class TreeItem extends UIComponent, TreeItemState> { accessibility: accessibility.childBehaviors ? accessibility.childBehaviors.title : undefined, - }, + }), render: renderItemTitle, overrideProps: this.handleTitleOverrides, }) diff --git a/packages/react/src/lib/Telemetry.ts b/packages/react/src/lib/Telemetry.ts new file mode 100644 index 0000000000..f4a9792693 --- /dev/null +++ b/packages/react/src/lib/Telemetry.ts @@ -0,0 +1,20 @@ +type ComponentPerfStats = { + count: number + msTotal: number + msMin: number + msMax: number +} + +export default class Telemetry { + performance: Record + enabled: boolean + + constructor() { + this.performance = {} + this.enabled = true + } + + reset() { + this.performance = {} + } +} diff --git a/packages/react/src/lib/factories.ts b/packages/react/src/lib/factories.ts index 11d0e2cc5c..d9ec24c03d 100644 --- a/packages/react/src/lib/factories.ts +++ b/packages/react/src/lib/factories.ts @@ -16,7 +16,7 @@ type ShorthandProp = 'children' | 'src' | 'type' interface CreateShorthandOptions

{ /** Default props object */ - defaultProps?: Partial> + defaultProps?: () => Partial> /** Override props object or function (called with regular props) */ overrideProps?: Partial> | ((props: P) => Partial>) @@ -194,7 +194,7 @@ function createShorthandFromValue

({ // ---------------------------------------- // Build up props // ---------------------------------------- - const defaultProps = options.defaultProps || ({} as Props

) + const defaultProps = options.defaultProps ? options.defaultProps() : ({} as Props

) // User's props const usersProps = diff --git a/packages/react/src/lib/index.ts b/packages/react/src/lib/index.ts index eb238a869b..81b5c785d2 100644 --- a/packages/react/src/lib/index.ts +++ b/packages/react/src/lib/index.ts @@ -41,3 +41,4 @@ export * from './commonPropInterfaces' export { commonPropTypes } export { default as withDebugId } from './withDebugId' +export { default as Telemetry } from './Telemetry' diff --git a/packages/react/src/lib/mergeProviderContexts.ts b/packages/react/src/lib/mergeProviderContexts.ts index f838036a38..752bf6df81 100644 --- a/packages/react/src/lib/mergeProviderContexts.ts +++ b/packages/react/src/lib/mergeProviderContexts.ts @@ -53,8 +53,8 @@ const mergeProviderContexts = ( }, rtl: false, disableAnimations: false, - originalThemes: [], target: document, // eslint-disable-line no-undef + telemetry: undefined, _internal_resolvedComponentVariables: {}, renderer: undefined, } @@ -84,11 +84,7 @@ const mergeProviderContexts = ( acc.disableAnimations = mergedDisableAnimations } - const contextOriginalThemes = (next as ProviderContextPrepared).originalThemes - ? (next as ProviderContextPrepared).originalThemes - : [next.theme] - - acc.originalThemes = [...acc.originalThemes, ...contextOriginalThemes] + acc.telemetry = next.telemetry || acc.telemetry return acc }, diff --git a/packages/react/src/lib/mergeThemes.ts b/packages/react/src/lib/mergeThemes.ts index 848ecacdc1..f199c8a5e1 100644 --- a/packages/react/src/lib/mergeThemes.ts +++ b/packages/react/src/lib/mergeThemes.ts @@ -1,23 +1,23 @@ import { callable } from '@stardust-ui/react-bindings' import * as _ from 'lodash' import { - ComponentVariablesInput, - ComponentVariablesPrepared, + ComponentSlotStyle, ComponentSlotStylesInput, ComponentSlotStylesPrepared, + ComponentVariablesInput, + ComponentVariablesPrepared, FontFace, SiteVariablesInput, SiteVariablesPrepared, + StaticStyle, + ThemeAnimation, ThemeComponentStylesInput, ThemeComponentStylesPrepared, ThemeComponentVariablesInput, ThemeComponentVariablesPrepared, + ThemeIcons, ThemeInput, ThemePrepared, - StaticStyle, - ThemeIcons, - ComponentSlotStyle, - ThemeAnimation, } from '../themes/types' import toCompactArray from './toCompactArray' import deepmerge from './deepmerge' @@ -57,8 +57,26 @@ export const mergeComponentStyles__PROD = ( const originalTarget = partStylesPrepared[partName] const originalSource = partStyle + // if there is no source, merging is a no-op, skip it + if ( + typeof originalSource === 'undefined' || + originalSource === null || + (typeof originalSource === 'object' && Object.keys(originalSource).length === 0) + ) { + return + } + + // no target means source doesn't need to merge onto anything + // just ensure source is callable (prepared format) + if (typeof originalTarget === 'undefined') { + partStylesPrepared[partName] = callable(originalSource) + return + } + + // We have both target and source, replace with merge fn partStylesPrepared[partName] = styleParam => { - return _.merge(callable(originalTarget)(styleParam), callable(originalSource)(styleParam)) + // originalTarget is always prepared, fn is guaranteed + return _.merge(originalTarget(styleParam), callable(originalSource)(styleParam)) } }) @@ -81,9 +99,36 @@ export const mergeComponentStyles__DEV = ( const originalTarget = partStylesPrepared[partName] const originalSource = partStyle + // if there is no source, merging is a no-op, skip it + if ( + typeof originalSource === 'undefined' || + originalSource === null || + (typeof originalSource === 'object' && Object.keys(originalSource).length === 0) + ) { + return + } + + // no target means source doesn't need to merge onto anything + // just ensure source is callable (prepared format) and has _debug + if (typeof originalTarget === 'undefined') { + partStylesPrepared[partName] = styleParam => { + // originalTarget is always prepared, fn is guaranteed, _debug always exists + const { _debug = undefined, ...styles } = callable(originalSource)(styleParam) || {} + + // new object required to prevent circular JSON structure error in + return { + ...styles, + _debug: _debug || [{ styles: { ...styles }, debugId: stylesByPart._debugId }], + } + } + + return + } + + // We have both target and source, replace with merge fn partStylesPrepared[partName] = styleParam => { - const { _debug: targetDebug = [], ...targetStyles } = - callable(originalTarget)(styleParam) || {} + // originalTarget is always prepared, fn is guaranteed, _debug always exists + const { _debug: targetDebug, ...targetStyles } = originalTarget(styleParam) const { _debug: sourceDebug = undefined, ...sourceStyles } = callable(originalSource)(styleParam) || {} diff --git a/packages/react/src/lib/renderComponent.tsx b/packages/react/src/lib/renderComponent.tsx index e51f56e214..1d1c89f181 100644 --- a/packages/react/src/lib/renderComponent.tsx +++ b/packages/react/src/lib/renderComponent.tsx @@ -34,6 +34,8 @@ import createAnimationStyles from './createAnimationStyles' import { isEnabled as isDebugEnabled } from './debug/debugEnabled' import { DebugData } from './debug/debugData' import withDebugId from './withDebugId' +import Telemetry from './Telemetry' +import resolveStylesAndClasses from './resolveStylesAndClasses' export interface RenderResultConfig

{ ElementType: React.ElementType

@@ -175,9 +177,12 @@ const renderComponent =

( renderer = null, rtl = false, theme = emptyTheme, + telemetry = undefined as Telemetry, _internal_resolvedComponentVariables: resolvedComponentVariables = {}, } = context || {} + const startTime = telemetry && telemetry.enabled ? performance.now() : 0 + const ElementType = getElementType(props) as React.ReactType

const stateAndProps = { ...state, ...props } @@ -234,22 +239,11 @@ const renderComponent =

( displayName, // does not affect styles, only used by useEnhancedRenderer in docs } - const resolvedStyles: ComponentSlotStylesPrepared = {} - const resolvedStylesDebug: { [key: string]: { styles: Object }[] } = {} - const classes: ComponentSlotClasses = {} - - Object.keys(mergedStyles).forEach(slotName => { - resolvedStyles[slotName] = callable(mergedStyles[slotName])(styleParam) - - if (process.env.NODE_ENV !== 'production' && isDebugEnabled) { - resolvedStylesDebug[slotName] = resolvedStyles[slotName]['_debug'] - delete resolvedStyles[slotName]['_debug'] - } - - if (renderer) { - classes[slotName] = renderer.renderRule(callable(resolvedStyles[slotName]), felaParam) - } - }) + const { resolvedStyles, resolvedStylesDebug, classes } = resolveStylesAndClasses( + mergedStyles, + styleParam, + renderer ? style => renderer.renderRule(() => style, felaParam) : undefined, + ) classes.root = cx(className, classes.root, props.className) @@ -264,6 +258,13 @@ const renderComponent =

( theme, } + let result + if (accessibility.focusZone) { + result = renderWithFocusZone(render, accessibility.focusZone, resolvedConfig) + } else { + result = render(resolvedConfig) + } + // conditionally add sources for evaluating debug information to component if (process.env.NODE_ENV !== 'production' && isDebugEnabled) { saveDebug({ @@ -272,11 +273,7 @@ const renderComponent =

( resolvedVariables._debug, variables => !_.isEmpty(variables.resolved), ), - componentStyles: _.mapValues(resolvedStylesDebug, v => - _.filter(v, v => { - return !_.isEmpty(v.styles) - }), - ), + componentStyles: resolvedStylesDebug, siteVariables: _.filter(theme.siteVariables._debug, siteVars => { if (_.isEmpty(siteVars) || _.isEmpty(siteVars.resolved)) { return false @@ -296,11 +293,31 @@ const renderComponent =

( }) } - if (accessibility.focusZone) { - return renderWithFocusZone(render, accessibility.focusZone, resolvedConfig) + if (telemetry && telemetry.enabled) { + const duration = performance.now() - startTime + + if (telemetry.performance[displayName]) { + telemetry.performance[displayName].count++ + telemetry.performance[displayName].msTotal += duration + telemetry.performance[displayName].msMin = Math.min( + duration, + telemetry.performance[displayName].msMin, + ) + telemetry.performance[displayName].msMax = Math.max( + duration, + telemetry.performance[displayName].msMax, + ) + } else { + telemetry.performance[displayName] = { + count: 1, + msTotal: duration, + msMin: duration, + msMax: duration, + } + } } - return render(resolvedConfig) + return result } export default renderComponent diff --git a/packages/react/src/lib/resolveStylesAndClasses.ts b/packages/react/src/lib/resolveStylesAndClasses.ts new file mode 100644 index 0000000000..e05cb06e4c --- /dev/null +++ b/packages/react/src/lib/resolveStylesAndClasses.ts @@ -0,0 +1,77 @@ +import { ComponentSlotClasses, ComponentSlotStylesPrepared, ICSSInJSStyle } from '../themes/types' +import { isEnabled as isDebugEnabled } from './debug/debugEnabled' + +// Both resolvedStyles and classes are objects of getters with lazy evaluation +const resolveStylesAndClasses = ( + mergedStyles: ComponentSlotStylesPrepared, + styleParam, + renderStyles, +): { + resolvedStyles: ICSSInJSStyle + resolvedStylesDebug: { [key: string]: { styles: Object }[] } + classes: ComponentSlotClasses +} => { + const resolvedStyles = {} + const resolvedStylesDebug = {} + const classes = {} + + Object.keys(mergedStyles).forEach(slotName => { + // resolve/render slot styles once and cache + const cacheKey = `${slotName}__return` + + Object.defineProperty(resolvedStyles, slotName, { + enumerable: false, + configurable: false, + set(val) { + resolvedStyles[cacheKey] = val + return true + }, + get() { + if (resolvedStyles[cacheKey]) { + return resolvedStyles[cacheKey] + } + + // resolve/render slot styles once and cache + resolvedStyles[cacheKey] = mergedStyles[slotName](styleParam) + + if (process.env.NODE_ENV !== 'production' && isDebugEnabled) { + resolvedStylesDebug[slotName] = resolvedStyles[slotName]['_debug'] + delete resolvedStyles[slotName]['_debug'] + } + + return resolvedStyles[cacheKey] + }, + }) + + Object.defineProperty(classes, slotName, { + enumerable: false, + configurable: false, + set(val) { + classes[cacheKey] = val + return true + }, + get() { + if (classes[cacheKey]) { + return classes[cacheKey] + } + + // this resolves the getter magic + const styleObj = resolvedStyles[slotName] + + if (renderStyles && styleObj) { + classes[cacheKey] = renderStyles(styleObj) + } + + return classes[cacheKey] + }, + }) + }) + + return { + resolvedStyles, + resolvedStylesDebug, + classes, + } +} + +export default resolveStylesAndClasses diff --git a/packages/react/src/lib/whatInput.ts b/packages/react/src/lib/whatInput.ts index 7d85e85637..f1d18fb5a2 100644 --- a/packages/react/src/lib/whatInput.ts +++ b/packages/react/src/lib/whatInput.ts @@ -30,6 +30,8 @@ const ignoreMap = [ 91, // Windows key / left Apple cmd 93, // Windows menu / right Apple cmd ] +// used to count how many Providers needed to initialize whatinput. +const whatInputInitialized = 'whatInputInitialized' // mapping of events to input types const inputMap = { @@ -87,7 +89,7 @@ const addListeners = (eventTarget: Window) => { // `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding // can only demonstrate potential, but not actual, interaction // and are treated separately - const options = supportsPassive ? { passive: true, useCapture: true } : true + const options = supportsPassive ? { passive: true, capture: true } : true // pointer events (mouse, pen, touch) // @ts-ignore @@ -212,17 +214,55 @@ export const setUpWhatInput = (target: Document) => { 'addEventListener' in targetWindow && Array.prototype.indexOf ) { - const whatInputInitialized = 'whatInputInitialized' - if (target[whatInputInitialized] === true) { + const initializedTimes = target[whatInputInitialized] + if (typeof initializedTimes === 'number' && initializedTimes > 0) { + target[whatInputInitialized] = initializedTimes + 1 return } - target[whatInputInitialized] = true + target[whatInputInitialized] = 1 addListeners(targetWindow) doUpdate(target) } } +function cleanupWhatInput(eventTarget: Window) { + const options = supportsPassive ? { capture: true } : true + + // @ts-ignore + if (eventTarget.PointerEvent) { + eventTarget.removeEventListener('pointerdown', setInput) + // @ts-ignore + } else if (window.MSPointerEvent) { + eventTarget.removeEventListener('MSPointerDown', setInput) + } else { + // mouse events + eventTarget.removeEventListener('mousedown', setInput, true) + + // touch events + if ('ontouchstart' in eventTarget) { + eventTarget.removeEventListener('touchstart', eventBuffer, options) + eventTarget.removeEventListener('touchend', setInput, true) + } + } + + // keyboard events + eventTarget.removeEventListener('keydown', eventBuffer, true) + eventTarget.removeEventListener('keyup', eventBuffer, true) +} + +export const tryCleanupWhatInput = (target: Document) => { + const targetWindow = target.defaultView + if (isBrowser() && targetWindow && 'removeEventListener' in targetWindow) { + if (target[whatInputInitialized] === 1) { + delete target[whatInputInitialized] + cleanupWhatInput(targetWindow) + } else { + target[whatInputInitialized] = target[whatInputInitialized] - 1 + } + } +} + export const setWhatInputSource = (newInput: 'mouse' | 'keyboard' | 'initial') => { currentInput = newInput } diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 8fcfb55246..00d14f8544 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -4,6 +4,7 @@ import * as React from 'react' import { ThemeInput, Renderer, ThemePrepared } from './themes/types' +import Telemetry from './lib/Telemetry' export type Extendable = T & { [key: string]: V @@ -159,6 +160,7 @@ export interface ProviderContextInput { disableAnimations?: boolean target?: Document theme?: ThemeInput + telemetry?: Telemetry } export interface ProviderContextPrepared { @@ -167,6 +169,6 @@ export interface ProviderContextPrepared { disableAnimations: boolean target: Document theme: ThemePrepared - originalThemes: (ThemeInput | undefined)[] + telemetry: Telemetry | undefined _internal_resolvedComponentVariables: Record } diff --git a/packages/react/test/specs/components/Provider/Provider-test.tsx b/packages/react/test/specs/components/Provider/Provider-test.tsx index 9efe33ce85..bd1b08d984 100644 --- a/packages/react/test/specs/components/Provider/Provider-test.tsx +++ b/packages/react/test/specs/components/Provider/Provider-test.tsx @@ -232,4 +232,60 @@ describe('Provider', () => { expect(renderStatic).toHaveBeenCalled() }) + + describe('target', () => { + test('performs whatinput init on first Provider mount', () => { + const addEventListener = jest.fn() + const setAttribute = jest.fn() + const externalDocument: any = { + defaultView: { + addEventListener, + removeEventListener: jest.fn(), + ontouchstart: jest.fn(), + }, + documentElement: { + setAttribute, + }, + } + + mount( + + +

+ + , + ) + + // mousedown + touchstart + touchend + keyup + keydown + expect(addEventListener).toHaveBeenCalledTimes(5) + expect(setAttribute).toHaveBeenCalledWith('data-whatinput', expect.any(String)) + }) + + test('performs whatinput cleanup on last Provider unmount', () => { + const removeEventListener = jest.fn() + const setAttribute = jest.fn() + const externalDocument: any = { + defaultView: { + addEventListener: jest.fn(), + removeEventListener, + ontouchstart: jest.fn(), + }, + documentElement: { + setAttribute, + }, + } + + const wrapper = mount( + + +
+ + , + ) + wrapper.unmount() + + // mousedown + touchstart + touchend + keyup + keydown + expect(removeEventListener).toHaveBeenCalledTimes(5) + }) + }) }) diff --git a/packages/react/test/specs/lib/factories-test.tsx b/packages/react/test/specs/lib/factories-test.tsx index 108f386461..c3da028ce2 100644 --- a/packages/react/test/specs/lib/factories-test.tsx +++ b/packages/react/test/specs/lib/factories-test.tsx @@ -12,7 +12,7 @@ import { consoleUtil } from 'test/utils' type ShorthandConfig = { Component?: React.ReactType - defaultProps?: Props + defaultProps?: () => Props mappedProp?: string mappedArrayProp?: string overrideProps?: Props & ((props: Props) => Props) | Props @@ -67,7 +67,9 @@ const itReturnsNull = valueOrRenderCallback => { const itReturnsNullGivenDefaultProps = valueOrRenderCallback => { test('returns null given defaultProps object', () => { consoleUtil.disableOnce() - expect(getShorthand({ valueOrRenderCallback, defaultProps: { 'data-foo': 'foo' } })).toBe(null) + expect( + getShorthand({ valueOrRenderCallback, defaultProps: () => ({ 'data-foo': 'foo' }) }), + ).toBe(null) }) } @@ -79,12 +81,15 @@ const itReturnsAValidElement = valueOrRenderCallback => { const itAppliesDefaultProps = (valueOrRenderCallback: ShorthandValue) => { test('applies defaultProps', () => { - const defaultProps = { some: 'defaults' } + const defaultPropsValue = { some: 'defaults' } const expectedResult = isValuePrimitive(valueOrRenderCallback) - ? { ...defaultProps, children: valueOrRenderCallback } - : defaultProps + ? { ...defaultPropsValue, children: valueOrRenderCallback } + : defaultPropsValue - testCreateShorthand({ valueOrRenderCallback, defaultProps }, expectedResult) + testCreateShorthand( + { valueOrRenderCallback, defaultProps: () => defaultPropsValue }, + expectedResult, + ) }) } @@ -103,7 +108,7 @@ const itMergesClassNames = ( shorthandConfig: { valueOrRenderCallback?: ShorthandValue; mappedProp?: string }, ) => { test(`merges defaultProps className and ${classNameSource} className`, () => { - const defaultProps = { className: 'default' } + const defaultProps = () => ({ className: 'default' }) const overrideProps = { className: 'override' } let expectedClassNames = 'default override' @@ -143,7 +148,7 @@ const mappedProps = { const itOverridesDefaultPropsWithFalseyProps = (propsSource, shorthandConfig) => { test(`overrides defaultProps with falsey ${propsSource} props`, () => { - const defaultProps = { undef: '-', nil: '-', zero: '-', empty: '-' } + const defaultProps = () => ({ undef: '-', nil: '-', zero: '-', empty: '-' }) const expectedProps = { undef: undefined, nil: null, zero: 0, empty: '' } testCreateShorthand({ defaultProps, ...shorthandConfig }, expectedProps) @@ -225,9 +230,9 @@ describe('factories', () => { getShorthand({ valueOrRenderCallback, Component: 'div', - defaultProps: { + defaultProps: () => ({ baz: 'original', - }, + }), overrideProps: { baz: 'overriden', }, @@ -281,12 +286,12 @@ describe('factories', () => { test('deep merges styles prop onto defaultProps styles', () => { expect.assertions(1) - const defaultProps = { + const defaultProps = () => ({ styles: { color: 'override me', ':hover': { color: 'blue' }, }, - } + }) const props = { styles: { color: 'black' }, } @@ -346,12 +351,12 @@ describe('factories', () => { test('deep merges styles prop as function onto defaultProps styles', () => { expect.assertions(1) - const defaultProps = { + const defaultProps = () => ({ styles: () => ({ color: 'override me', ':hover': { color: 'blue' }, }), - } + }) const props = { styles: { color: 'black' }, } @@ -411,10 +416,10 @@ describe('factories', () => { describe('defaultProps', () => { test('can be an object', () => { - const defaultProps = { 'data-some': 'defaults' } + const defaultPropsValue = { 'data-some': 'defaults' } testCreateShorthand( - { defaultProps, valueOrRenderCallback: 'foo' }, - { ...defaultProps, children: 'foo' }, + { defaultProps: () => defaultPropsValue, valueOrRenderCallback: 'foo' }, + { ...defaultPropsValue, children: 'foo' }, ) }) }) @@ -532,20 +537,32 @@ describe('factories', () => { }) test("is called with the user's element's and default props", () => { - const defaultProps = { 'data-some': 'defaults' } + const defaultPropsValue = { 'data-some': 'defaults' } const overrideProps = jest.fn(() => ({})) - shallow(getShorthand({ defaultProps, overrideProps, valueOrRenderCallback:
})) - expect(overrideProps).toHaveBeenCalledWith(defaultProps) + shallow( + getShorthand({ + defaultProps: () => defaultPropsValue, + overrideProps, + valueOrRenderCallback:
, + }), + ) + expect(overrideProps).toHaveBeenCalledWith(defaultPropsValue) }) test("is called with the user's props object", () => { - const defaultProps = { 'data-some': 'defaults' } + const defaultPropsValue = { 'data-some': 'defaults' } const overrideProps = jest.fn(() => ({})) const userProps = { 'data-user': 'props' } - shallow(getShorthand({ defaultProps, overrideProps, valueOrRenderCallback: userProps })) - expect(overrideProps).toHaveBeenCalledWith({ ...defaultProps, ...userProps }) + shallow( + getShorthand({ + defaultProps: () => defaultPropsValue, + overrideProps, + valueOrRenderCallback: userProps, + }), + ) + expect(overrideProps).toHaveBeenCalledWith({ ...defaultPropsValue, ...userProps }) }) }) @@ -587,7 +604,7 @@ describe('factories', () => { ) itOverridesDefaultProps( 'mappedProp', - { some: 'defaults', overridden: null }, + () => ({ some: 'defaults', overridden: null }), { some: 'defaults', overridden:
}, { valueOrRenderCallback:
, @@ -615,7 +632,7 @@ describe('factories', () => { itOverridesDefaultProps( 'mappedProp', - { some: 'defaults', overridden: 'false' }, + () => ({ some: 'defaults', overridden: 'false' }), { some: 'defaults', overridden: 'true' }, { valueOrRenderCallback: 'true', @@ -633,7 +650,7 @@ describe('factories', () => { describe(`'${as}' as 'as' prop to defaultProps`, () => { test(`overrides ${mappedProp} and ${testMsg}`, () => { testCreateShorthand( - { mappedProp, valueOrRenderCallback: value, defaultProps: { as } }, + { mappedProp, valueOrRenderCallback: value, defaultProps: () => ({ as }) }, { as, [mappedProps[as]]: value }, ) }) @@ -654,7 +671,7 @@ describe('factories', () => { { mappedProp, valueOrRenderCallback: value, - defaultProps: { as: 'overriden' }, + defaultProps: () => ({ as: 'overriden' }), overrideProps: { as }, }, { as, [mappedProps[as]]: value }, @@ -670,7 +687,11 @@ describe('factories', () => { describe(`and an unsupported tag as 'as' prop to defaultProps`, () => { test(testMsg, () => { testCreateShorthand( - { mappedProp, valueOrRenderCallback: value, defaultProps: { as: 'unsupported' } }, + { + mappedProp, + valueOrRenderCallback: value, + defaultProps: () => ({ as: 'unsupported' }), + }, { as: 'unsupported', [mappedProp]: value }, ) }) @@ -691,7 +712,7 @@ describe('factories', () => { { mappedProp, valueOrRenderCallback: value, - defaultProps: { as: 'div' }, + defaultProps: () => ({ as: 'div' }), overrideProps: { as: 'unsupported' }, }, { as: 'unsupported', [mappedProp]: value }, @@ -706,7 +727,7 @@ describe('factories', () => { describe(`and an unsupported tag as 'as' prop to defaultProps`, () => { test(testMsg, () => { testCreateShorthand( - { valueOrRenderCallback: value, defaultProps: { as: 'unsupported' } }, + { valueOrRenderCallback: value, defaultProps: () => ({ as: 'unsupported' }) }, { as: 'unsupported', children: value }, ) }) @@ -726,7 +747,7 @@ describe('factories', () => { testCreateShorthand( { valueOrRenderCallback: value, - defaultProps: { as: 'div' }, + defaultProps: () => ({ as: 'div' }), overrideProps: { as: 'unsupported' }, }, { as: 'unsupported', children: value }, @@ -746,7 +767,7 @@ describe('factories', () => { itOverridesDefaultProps( 'props object', - { some: 'defaults', overridden: false }, + () => ({ some: 'defaults', overridden: false }), { some: 'defaults', overridden: true }, { valueOrRenderCallback: { overridden: true }, @@ -771,7 +792,7 @@ describe('factories', () => { { mappedArrayProp, valueOrRenderCallback: value, - defaultProps: { as: 'unsupported' }, + defaultProps: () => ({ as: 'unsupported' }), }, { as: 'unsupported', [mappedArrayProp]: value }, ) @@ -782,7 +803,7 @@ describe('factories', () => { describe('style', () => { test('merges style prop', () => { - const defaultProps = { style: { left: 5 } } + const defaultProps = () => ({ style: { left: 5 } }) const userProps = { style: { bottom: 5 } } const overrideProps = { style: { right: 5 } } @@ -794,7 +815,7 @@ describe('factories', () => { }) test('merges style prop and handles override by userProps', () => { - const defaultProps = { style: { left: 10, bottom: 5 } } + const defaultProps = () => ({ style: { left: 10, bottom: 5 } }) const userProps = { style: { bottom: 10 } } expect( @@ -818,7 +839,7 @@ describe('factories', () => { }) test('merges style prop from defaultProps and overrideProps', () => { - const defaultProps = { style: { left: 10, bottom: 5 } } + const defaultProps = () => ({ style: { left: 10, bottom: 5 } }) const overrideProps = { style: { bottom: 10 } } expect( diff --git a/packages/react/test/specs/lib/mergeThemes/mergeComponentStyles-test.ts b/packages/react/test/specs/lib/mergeThemes/mergeComponentStyles-test.ts index b21b8e5a21..4a25d7a3f2 100644 --- a/packages/react/test/specs/lib/mergeThemes/mergeComponentStyles-test.ts +++ b/packages/react/test/specs/lib/mergeThemes/mergeComponentStyles-test.ts @@ -59,23 +59,40 @@ describe('mergeComponentStyles', () => { expect(() => mergeComponentStyles(stylesWithUndefined, styles)).not.toThrow() }) - test('component parts are merged', () => { + test('component parts without styles are not merged', () => { const target = { root: {} } const source = { icon: {} } const merged = mergeComponentStyles(target, source) + expect(merged).not.toHaveProperty('root') + expect(merged).not.toHaveProperty('icon') + }) + + test('component parts with style properties are merged', () => { + const target = { root: { color: 'red' } } + const source = { icon: { color: 'red' } } + + const merged = mergeComponentStyles(target, source) + expect(merged).toHaveProperty('root') expect(merged).toHaveProperty('icon') }) - test('component part objects are converted to functions', () => { - const target = { root: {} } - const source = { root: {} } + test('converts merged component parts to functions', () => { + const target = { root: { color: 'red' } } + const source = { root: { color: 'red' } } const merged = mergeComponentStyles(target, source) expect(merged.root).toBeInstanceOf(Function) + }) + + test('converts target only component parts to functions', () => { + const target = { root: { color: 'red' } } + + const merged = mergeComponentStyles(target) + expect(merged.root).toBeInstanceOf(Function) }) diff --git a/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts b/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts index 8dd1f7dee9..b91e16d343 100644 --- a/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts +++ b/packages/react/test/specs/lib/mergeThemes/mergeThemes-test.ts @@ -171,19 +171,29 @@ describe('mergeThemes', () => { expect(merged.componentStyles).toHaveProperty('Icon') }) - test('component parts are merged', () => { + test('component parts without styles are not merged', () => { const target = { componentStyles: { Button: { root: {} } } } const source = { componentStyles: { Button: { icon: {} } } } const merged = mergeThemes(target, source) + expect(merged.componentStyles.Button).not.toHaveProperty('root') + expect(merged.componentStyles.Button).not.toHaveProperty('icon') + }) + + test('component parts with style properties are merged', () => { + const target = { componentStyles: { Button: { root: { color: 'red' } } } } + const source = { componentStyles: { Icon: { root: { color: 'red' } } } } + + const merged = mergeThemes(target, source) + expect(merged.componentStyles.Button).toHaveProperty('root') - expect(merged.componentStyles.Button).toHaveProperty('icon') + expect(merged.componentStyles.Icon).toHaveProperty('root') }) - test('component part objects are converted to functions', () => { - const target = { componentStyles: { Button: { root: {} } } } - const source = { componentStyles: { Icon: { root: {} } } } + test('converts merged component parts to functions', () => { + const target = { componentStyles: { Button: { root: { color: 'red' } } } } + const source = { componentStyles: { Icon: { root: { color: 'red' } } } } const merged = mergeThemes(target, source) @@ -191,6 +201,14 @@ describe('mergeThemes', () => { expect(merged.componentStyles.Icon.root).toBeInstanceOf(Function) }) + test('converts target only component parts to functions', () => { + const target = { componentStyles: { Button: { root: { color: 'red' } } } } + + const merged = mergeThemes(target) + + expect(merged.componentStyles.Button.root).toBeInstanceOf(Function) + }) + test('component part styles are deeply merged', () => { const target = { componentStyles: { diff --git a/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts b/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts new file mode 100644 index 0000000000..f2580aa0fe --- /dev/null +++ b/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts @@ -0,0 +1,58 @@ +import resolveStylesAndClasses from 'src/lib/resolveStylesAndClasses' +import { ComponentSlotStylesPrepared } from 'src/themes/types' + +describe('resolveStylesAndClasses', () => { + const styleParam = { + variables: { + color: 'red', + }, + } + + const componentStyles: ComponentSlotStylesPrepared = { + root: ({ variables }) => ({ + color: variables['color'], + }), + } + + test('resolves styles', () => { + const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, () => ({})) + + expect(resolvedStyles.root).toMatchObject({ color: 'red' }) + }) + + test('caches resolved styles', () => { + spyOn(componentStyles, 'root').and.callThrough() + const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, () => ({})) + + expect(resolvedStyles.root).toMatchObject({ color: 'red' }) + expect(componentStyles.root).toHaveBeenCalledTimes(1) + expect(resolvedStyles.root).toMatchObject({ color: 'red' }) + expect(componentStyles.root).toHaveBeenCalledTimes(1) + }) + + test('does not render classes if not fetched', () => { + const renderStyles = jest.fn() + const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles) + + expect(resolvedStyles.root).toMatchObject({ color: 'red' }) + expect(renderStyles).not.toBeCalled() + }) + + test('renders classes when slot classes getter is accessed', () => { + const renderStyles = jest.fn().mockReturnValue('a') + const { classes } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles) + + expect(classes.root).toBeDefined() + expect(renderStyles).toHaveBeenCalledWith({ color: 'red' }) + }) + + test('caches rendered classes', () => { + const renderStyles = jest.fn().mockReturnValue('a') + const { classes } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles) + + expect(classes.root).toBeDefined() + expect(renderStyles).toHaveBeenCalledWith({ color: 'red' }) + expect(classes.root).toBeDefined() + expect(renderStyles).toHaveBeenCalledTimes(1) + }) +})