diff --git a/.yarn/cache/@telefonica-eslint-config-npm-2.1.1-4cc790ac85-8f5d1e7189.zip b/.yarn/cache/@telefonica-eslint-config-npm-2.2.0-d45d42ae4d-7e82d56454.zip similarity index 53% rename from .yarn/cache/@telefonica-eslint-config-npm-2.1.1-4cc790ac85-8f5d1e7189.zip rename to .yarn/cache/@telefonica-eslint-config-npm-2.2.0-d45d42ae4d-7e82d56454.zip index 747aa54677..5b7358ae95 100644 Binary files a/.yarn/cache/@telefonica-eslint-config-npm-2.1.1-4cc790ac85-8f5d1e7189.zip and b/.yarn/cache/@telefonica-eslint-config-npm-2.2.0-d45d42ae4d-7e82d56454.zip differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f52b78773..ff5a374381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# [15.20.0](https://github.com/Telefonica/mistica-web/compare/v15.19.0...v15.20.0) (2024-09-06) + + +### Features + +* **NavigationBreadcrumbs:** allow executing onNavigate when pressing a link ([#1233](https://github.com/Telefonica/mistica-web/issues/1233)) ([ec7ed8b](https://github.com/Telefonica/mistica-web/commit/ec7ed8b93d0b6892827263d35373896a8cf291b6)) + +# [15.19.0](https://github.com/Telefonica/mistica-web/compare/v15.18.0...v15.19.0) (2024-09-03) + + +### Bug Fixes + +* **Buttons:** avoid warnings related to change in order of react hooks ([#1229](https://github.com/Telefonica/mistica-web/issues/1229)) ([2dbc411](https://github.com/Telefonica/mistica-web/commit/2dbc411616f7276d832bb5007f41a37c9c93f69e)) +* **i18n:** revert text sizes improvement because it is breaking ([#1226](https://github.com/Telefonica/mistica-web/issues/1226)) ([79eb4a4](https://github.com/Telefonica/mistica-web/commit/79eb4a427ef9f8d5a91bf56c8434ca1417a77df3)) +* **Logo:** fix webpackChunkName magic comments ([#1214](https://github.com/Telefonica/mistica-web/issues/1214)) ([3d1f098](https://github.com/Telefonica/mistica-web/commit/3d1f098c6cbaf179b29de666ba824f1ae63dea9e)) +* **Vivinho char:** vivinho char in headings being read as a separated heading ([#1209](https://github.com/Telefonica/mistica-web/issues/1209)) ([f0f5fb0](https://github.com/Telefonica/mistica-web/commit/f0f5fb05b99fc594479a19c3037cc9dfc7446bab)) + + +### Features + +* **Buttons:** refactor code and fix spacing bug in loading buttonLink ([#1212](https://github.com/Telefonica/mistica-web/issues/1212)) ([640e429](https://github.com/Telefonica/mistica-web/commit/640e429c8ab4493c5da1f39bad5fccf90eacc373)) +* **i18n:** improve texts sizes ([#1204](https://github.com/Telefonica/mistica-web/issues/1204)) ([0345e7c](https://github.com/Telefonica/mistica-web/commit/0345e7cefd06b911377278b5928409cbed23f921)) +* **Logo:** Refactor logo to improve bundle size and loading times ([#1210](https://github.com/Telefonica/mistica-web/issues/1210)) ([15b77cb](https://github.com/Telefonica/mistica-web/commit/15b77cb8ab932f68fb124d66887b8d49169b0095)) +* **NavigationBar, FunnelNavigationBar, MainNavigationBar:** add alternative variant ([#1200](https://github.com/Telefonica/mistica-web/issues/1200)) ([eef87ec](https://github.com/Telefonica/mistica-web/commit/eef87ecdd32053f80f349aaf1598d8e61251b6eb)) + # [15.18.0](https://github.com/Telefonica/mistica-web/compare/v15.17.0...v15.18.0) (2024-08-20) diff --git a/package.json b/package.json index 0b46d28285..7d3ec4ca23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@telefonica/mistica", - "version": "15.18.0", + "version": "15.20.0", "license": "MIT", "repository": { "type": "git", @@ -80,7 +80,7 @@ "@swc/core": "^1.3.95", "@swc/jest": "^0.2.29", "@telefonica/acceptance-testing": "5.0.0", - "@telefonica/eslint-config": "^2.1.1", + "@telefonica/eslint-config": "^2.2.0", "@telefonica/prettier-config": "^2.0.0", "@testing-library/dom": "^10.2.0", "@testing-library/jest-dom": "^6.4.6", diff --git a/playroom/components/index.tsx b/playroom/components/index.tsx index 9caf9ecf34..0fc6704ad8 100644 --- a/playroom/components/index.tsx +++ b/playroom/components/index.tsx @@ -123,11 +123,11 @@ const PreviewToolsControls = React.forwardRef, + Icon: IconAppleOn as (props: IconProps) => JSX.Element, 'aria-label': 'Change platform to android', }} uncheckedProps={{ - Icon: IconAppleOff as React.FC, + Icon: IconAppleOff as (props: IconProps) => JSX.Element, 'aria-label': 'Change platform to iOS', }} checked={os === 'ios'} @@ -136,11 +136,11 @@ const PreviewToolsControls = React.forwardRef, + Icon: IconSun as (props: IconProps) => JSX.Element, 'aria-label': 'Switch to light mode', }} uncheckedProps={{ - Icon: IconMoon as React.FC, + Icon: IconMoon as (props: IconProps) => JSX.Element, 'aria-label': 'Switch to dark mode', }} checked={colorScheme === alternativeColorScheme} @@ -155,7 +155,7 @@ const PreviewToolsControls = React.forwardRef} + Icon={IconCode as (props: IconProps) => JSX.Element} onPress={onEditStoryPress} /> @@ -178,11 +178,11 @@ const PreviewToolsControls = React.forwardRef, + Icon: IconAppleOn as (props: IconProps) => JSX.Element, 'aria-label': 'Change platform to android', }} uncheckedProps={{ - Icon: IconAppleOff as React.FC, + Icon: IconAppleOff as (props: IconProps) => JSX.Element, 'aria-label': 'Change platform to iOS', }} checked={os === 'ios'} @@ -191,11 +191,11 @@ const PreviewToolsControls = React.forwardRef, + Icon: IconSun as (props: IconProps) => JSX.Element, 'aria-label': 'Change color scheme', }} uncheckedProps={{ - Icon: IconMoon as React.FC, + Icon: IconMoon as (props: IconProps) => JSX.Element, 'aria-label': 'Change color scheme', }} checked={colorScheme === alternativeColorScheme} @@ -210,7 +210,7 @@ const PreviewToolsControls = React.forwardRef} + Icon={IconCode as (props: IconProps) => JSX.Element} onPress={onEditStoryPress} /> diff --git a/playroom/components/loader.tsx b/playroom/components/loader.tsx index 089954c6f8..91c85224c3 100644 --- a/playroom/components/loader.tsx +++ b/playroom/components/loader.tsx @@ -3,13 +3,18 @@ import * as React from 'react'; type Props = { load: string | (() => Promise); render: (data: any) => React.ReactElement; - renderLoading?: () => React.ReactElement; - renderError?: () => React.ReactElement; + renderLoading?: () => React.ReactElement | null; + renderError?: () => React.ReactElement | null; }; type LoaderState = 'loading' | 'error' | 'success'; -const Loader: React.FC = ({load, render, renderLoading = () => null, renderError = () => null}) => { +const Loader = ({ + load, + render, + renderLoading = () => null, + renderError = () => null, +}: Props): JSX.Element | null => { const [loaderData, setLoaderData] = React.useState(null); const [loaderStatus, setLoaderStatus] = React.useState('loading'); diff --git a/playroom/icons/icon-apple-off.tsx b/playroom/icons/icon-apple-off.tsx index d846b9bcd4..ca93c008c7 100644 --- a/playroom/icons/icon-apple-off.tsx +++ b/playroom/icons/icon-apple-off.tsx @@ -6,7 +6,7 @@ type Props = { color?: string; }; -const IconSun: React.FC = ({size = 24, color}) => { +const IconSun = ({size = 24, color}: Props): JSX.Element => { return ( diff --git a/playroom/icons/icon-apple-on.tsx b/playroom/icons/icon-apple-on.tsx index 75276b9d1e..604d7bd250 100644 --- a/playroom/icons/icon-apple-on.tsx +++ b/playroom/icons/icon-apple-on.tsx @@ -6,7 +6,7 @@ type Props = { color?: string; }; -const IconSun: React.FC = ({size = 24, color}) => { +const IconSun = ({size = 24, color}: Props): JSX.Element => { return ( diff --git a/playroom/icons/icon-code.tsx b/playroom/icons/icon-code.tsx index c1112d6914..cb7b736d02 100644 --- a/playroom/icons/icon-code.tsx +++ b/playroom/icons/icon-code.tsx @@ -6,7 +6,7 @@ type Props = { color?: string; }; -const IconCode: React.FC = ({size = 24, color}) => { +const IconCode = ({size = 24, color}: Props): JSX.Element => { return ( = ({size = 24, color}) => { +const IconMoon = ({size = 24, color}: Props): JSX.Element => { return ( = ({size = 24, color}) => { +const IconSun = ({size = 24, color}: Props): JSX.Element => { return ( ( title, to: `/${title}`, }))} + logo={LOGO} right={ {}} aria-label="shopping cart with 2 items"> diff --git a/src/__acceptance_tests__/__ssr_pages__/main-navigation-bar.tsx b/src/__acceptance_tests__/__ssr_pages__/main-navigation-bar.tsx index 41cc4ddd8b..0bbb17cb0c 100644 --- a/src/__acceptance_tests__/__ssr_pages__/main-navigation-bar.tsx +++ b/src/__acceptance_tests__/__ssr_pages__/main-navigation-bar.tsx @@ -14,6 +14,7 @@ const NavigationBarTest = (): JSX.Element => ( title, to: `/${title}`, }))} + logo={LOGO} right={ {}} aria-label="shopping cart with 2 items"> diff --git a/src/__private_stories__/private-responsive-layout-scenarios-story.tsx b/src/__private_stories__/private-responsive-layout-scenarios-story.tsx index 542ffac290..6d517f6604 100644 --- a/src/__private_stories__/private-responsive-layout-scenarios-story.tsx +++ b/src/__private_stories__/private-responsive-layout-scenarios-story.tsx @@ -21,7 +21,7 @@ export default { parameters: {fullScreen: true}, }; -const WithTitle: React.FC<{title: string; children: React.ReactNode}> = ({title, children}) => ( +const WithTitle = ({title, children}: {title: string; children: React.ReactNode}) => ( {title} diff --git a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-android-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-android-1-snap.png index c54cdc969b..dbb8d4ee16 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-android-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-android-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-ios-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-ios-1-snap.png index 6852de7de7..bf31954714 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-ios-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-danger-spinner-mobile-ios-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-android-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-android-1-snap.png index 923bcff398..33d4ff099c 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-android-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-android-1-snap.png differ diff --git a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-ios-1-snap.png b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-ios-1-snap.png index 4b41d242eb..3bab0e6593 100644 Binary files a/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-ios-1-snap.png and b/src/__screenshot_tests__/__image_snapshots__/button-screenshot-test-tsx-buttons-link-button-spinner-mobile-ios-1-snap.png differ diff --git a/src/__stories__/button-layout-story.tsx b/src/__stories__/button-layout-story.tsx index aaafd5eb48..6733a2c2f2 100644 --- a/src/__stories__/button-layout-story.tsx +++ b/src/__stories__/button-layout-story.tsx @@ -12,7 +12,7 @@ type Props = { align?: 'center' | 'left' | 'right' | 'full-width'; }; -const Template: React.FC = ({align = 'full-width'}) => ( +const Template = ({align = 'full-width'}: Props) => ( One button diff --git a/src/__stories__/button-story.tsx b/src/__stories__/button-story.tsx index 514af9096c..d8b56afa73 100644 --- a/src/__stories__/button-story.tsx +++ b/src/__stories__/button-story.tsx @@ -76,7 +76,7 @@ type Props = { children: React.ReactNode; }; -const ButtonBackgroundContainer: React.FC = ({inverse, children}) => ( +const ButtonBackgroundContainer = ({inverse, children}: Props) => ( {children} diff --git a/src/__stories__/chip-story.tsx b/src/__stories__/chip-story.tsx index e2e1096e18..a67d148810 100644 --- a/src/__stories__/chip-story.tsx +++ b/src/__stories__/chip-story.tsx @@ -32,7 +32,7 @@ type Props = { dataAttributes: DataAttributes; }; -const ChipBackgroundContainer: React.FC = ({inverse, dataAttributes, children}) => ( +const ChipBackgroundContainer = ({inverse, dataAttributes, children}: Props) => (
= ({background, height, top}) => ( +const FixedDiv = ({background, height, top}: Props) => (
top: {top}, height: {height}
diff --git a/src/__stories__/form-error-handler-story.tsx b/src/__stories__/form-error-handler-story.tsx index ee85c665a2..bf3badbb44 100644 --- a/src/__stories__/form-error-handler-story.tsx +++ b/src/__stories__/form-error-handler-story.tsx @@ -7,7 +7,7 @@ export default { type CardProps = {show: boolean; children: React.ReactNode; onPress: any}; -const Card: React.FC = ({show, children, onPress}) => { +const Card = ({show, children, onPress}: CardProps) => { return ( <> @@ -18,7 +18,7 @@ const Card: React.FC = ({show, children, onPress}) => { ); }; -const Cards: React.FC = ({activeCard, setActiveCard}) => { +const Cards = ({activeCard, setActiveCard}: any) => { const {formErrors} = useForm(); return ( diff --git a/src/__stories__/helpers.tsx b/src/__stories__/helpers.tsx index 6d8c43a0cd..449a9803e8 100644 --- a/src/__stories__/helpers.tsx +++ b/src/__stories__/helpers.tsx @@ -7,7 +7,7 @@ type Props = { children: React.ReactNode; }; -export const StorySection: React.FC = ({title, children}) => { +export const StorySection = ({title, children}: Props): JSX.Element => { const isInverse = useIsInverseVariant(); return (
diff --git a/src/__stories__/story.d.ts b/src/__stories__/story.d.ts index 82000efc2f..1b3d616aa7 100644 --- a/src/__stories__/story.d.ts +++ b/src/__stories__/story.d.ts @@ -1,4 +1,4 @@ -declare type StoryComponent = React.FC & { +declare type StoryComponent = {(props: T): JSX.Element} & { storyName?: string; decorators?: Array; parameters?: {[name: string]: any}; diff --git a/src/__stories__/text-link-story.tsx b/src/__stories__/text-link-story.tsx index bc369dd831..22c9454602 100644 --- a/src/__stories__/text-link-story.tsx +++ b/src/__stories__/text-link-story.tsx @@ -27,7 +27,7 @@ interface TextStyleWrapperProps { children: React.ReactNode; } -const TextStyleWrapper: React.FC = ({children, textStyle}) => { +const TextStyleWrapper = ({children, textStyle}: TextStyleWrapperProps) => { if (textStyle === 'Text1') { return {children}; } diff --git a/src/__tests__/navigation-breadcrumbs-test.tsx b/src/__tests__/navigation-breadcrumbs-test.tsx new file mode 100644 index 0000000000..19345c49a3 --- /dev/null +++ b/src/__tests__/navigation-breadcrumbs-test.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import {makeTheme} from './test-utils'; +import NavigationBreadcrumbs from '../navigation-breadcrumbs'; +import userEvent from '@testing-library/user-event'; + +test('Breadcrumbs onNavigate is called when pressing a link', async () => { + const navigateSpy = jest.fn(); + + render( + + navigateSpy('breadcrumb 1')}, + {title: 'breadcrumb 2', url: '#', onNavigate: () => navigateSpy('breadcrumb 2')}, + ]} + /> + + ); + + const firstLink = await screen.findByRole('link', {name: 'breadcrumb 1'}); + const secondLink = await screen.findByRole('link', {name: 'breadcrumb 2'}); + + await userEvent.click(firstLink); + expect(navigateSpy).toHaveBeenLastCalledWith('breadcrumb 1'); + expect(navigateSpy).toHaveBeenCalledTimes(1); + + await userEvent.click(secondLink); + expect(navigateSpy).toHaveBeenLastCalledWith('breadcrumb 2'); + expect(navigateSpy).toHaveBeenCalledTimes(2); +}); diff --git a/src/__tests__/popover-test.tsx b/src/__tests__/popover-test.tsx index 779bf0182c..d48270cb90 100644 --- a/src/__tests__/popover-test.tsx +++ b/src/__tests__/popover-test.tsx @@ -6,7 +6,7 @@ import {makeTheme} from './test-utils'; type Props = Omit, 'description' | 'target'>; -const TestPopover: React.FC = ({children, ...props}) => ( +const TestPopover = ({children, ...props}: Props) => ( Press me!} /> diff --git a/src/__tests__/snackbar-test.tsx b/src/__tests__/snackbar-test.tsx index 0f4e8cd95f..57fc714d32 100644 --- a/src/__tests__/snackbar-test.tsx +++ b/src/__tests__/snackbar-test.tsx @@ -190,7 +190,7 @@ test('nativeMessage should be called once, even if the component re-renders', as const onCloseMock = jest.fn(); const nativeMessageMock = jest.spyOn(bridge, 'nativeMessage').mockResolvedValue(); - const ComponentWithSnackbar: React.FC<{onClose: () => unknown}> = ({onClose}) => { + const ComponentWithSnackbar = ({onClose}: {onClose: () => unknown}) => { const [count, setCount] = React.useState(0); return ( <> diff --git a/src/__tests__/tooltip-test.tsx b/src/__tests__/tooltip-test.tsx index a62f8352aa..62c5c3739f 100644 --- a/src/__tests__/tooltip-test.tsx +++ b/src/__tests__/tooltip-test.tsx @@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'; type Props = Omit, 'children' | 'target'>; -const TestTooltip: React.FC = (props) => ( +const TestTooltip = (props: Props) => ( ; -export const Accordion: React.FC = ({ +export const Accordion = ({ children, dataAttributes, index, @@ -238,7 +238,7 @@ export const Accordion: React.FC = ({ onChange, singleOpen, role, -}) => { +}: AccordionProps): JSX.Element => { const [indexList, toggle] = useAccordionState({ value: index, defaultValue: defaultIndex, @@ -285,7 +285,7 @@ export const BoxedAccordionItem = React.forwardRef = ({ +export const BoxedAccordion = ({ children, dataAttributes, index, @@ -293,7 +293,7 @@ export const BoxedAccordion: React.FC = ({ onChange, singleOpen, role, -}) => { +}: AccordionProps): JSX.Element => { const [indexList, toggle] = useAccordionState({ value: index, defaultValue: defaultIndex, diff --git a/src/avatar.tsx b/src/avatar.tsx index 5d67036446..42b1b3ce64 100644 --- a/src/avatar.tsx +++ b/src/avatar.tsx @@ -33,7 +33,7 @@ type AvatarProps = { initials?: string; textColor?: string; backgroundColor?: string; - Icon?: React.FC; + Icon?: (props: IconProps) => JSX.Element; badge?: boolean | number; 'aria-label'?: string; dataAttributes?: DataAttributes; diff --git a/src/badge.tsx b/src/badge.tsx index c4d7bcc25e..24a35b7deb 100644 --- a/src/badge.tsx +++ b/src/badge.tsx @@ -26,7 +26,7 @@ type Props = { * * */ -const Badge: React.FC = ({children, value, right, top, dataAttributes}) => { +const Badge = ({children, value, right, top, dataAttributes}: Props): JSX.Element | null => { const isInverse = useIsInverseVariant(); const {textPresets} = useTheme(); if (children && value === 0) { diff --git a/src/button-fixed-footer-layout.tsx b/src/button-fixed-footer-layout.tsx index 004f8927ab..277ea7a674 100644 --- a/src/button-fixed-footer-layout.tsx +++ b/src/button-fixed-footer-layout.tsx @@ -22,7 +22,7 @@ type Props = { onChangeFooterHeight?: (heightInPx: number) => void; }; -const ButtonFixedFooterLayout: React.FC = ({ +const ButtonFixedFooterLayout = ({ isFooterVisible = true, button, secondaryButton, @@ -32,7 +32,7 @@ const ButtonFixedFooterLayout: React.FC = ({ footerBgColor, containerBgColor, onChangeFooterHeight, -}) => { +}: Props): JSX.Element => { const {isDesktopOrBigger} = useScreenSize(); const hasButton = !!button || !!secondaryButton; return ( diff --git a/src/button-group.tsx b/src/button-group.tsx index b21d22f517..90a11c7303 100644 --- a/src/button-group.tsx +++ b/src/button-group.tsx @@ -14,13 +14,13 @@ export interface ButtonGroupProps { align?: ByBreakpoint<'center' | 'left'>; } -const ButtonGroup: React.FC = ({ +const ButtonGroup = ({ primaryButton, secondaryButton, link, align = 'left', dataAttributes, -}) => { +}: ButtonGroupProps): JSX.Element | null => { const anyAction = !!primaryButton || !!secondaryButton || !!link; const bothButtons = !!primaryButton && !!secondaryButton; diff --git a/src/button-layout.css.ts b/src/button-layout.css.ts index bb3d2d9beb..3c2aff0e77 100644 --- a/src/button-layout.css.ts +++ b/src/button-layout.css.ts @@ -1,7 +1,7 @@ import {style, globalStyle, styleVariants} from '@vanilla-extract/css'; import {sprinkles} from './sprinkles.css'; import * as mq from './media-queries.css'; -import {PADDING_X_LINK} from './button.css'; +import {buttonPaddingX, borderSize} from './button.css'; const buttonLayoutSpacing = 16; @@ -78,8 +78,12 @@ globalStyle(`${alignVariant['full-width']} > *:not(${linkBase})`, { }, }); -const bleedLeft = {marginLeft: buttonLayoutSpacing / 2 - PADDING_X_LINK}; -const bleedRight = {marginRight: buttonLayoutSpacing / 2 - PADDING_X_LINK}; +const bleedLeft = { + marginLeft: `calc(${buttonLayoutSpacing}px / 2 - (${buttonPaddingX.small} + ${borderSize}))`, +}; +const bleedRight = { + marginRight: `calc(${buttonLayoutSpacing}px / 2 - (${buttonPaddingX.small} + ${borderSize}))`, +}; export const link = style([ linkBase, diff --git a/src/button-layout.tsx b/src/button-layout.tsx index b182c519a9..fb0056b049 100644 --- a/src/button-layout.tsx +++ b/src/button-layout.tsx @@ -24,7 +24,7 @@ type ButtonLayoutProps = { const buttonsRange = [ButtonPrimary, ButtonDanger, ButtonSecondary]; -const ButtonLayout: React.FC = ({ +const ButtonLayout = ({ children, primaryButton, secondaryButton, @@ -32,7 +32,7 @@ const ButtonLayout: React.FC = ({ link, withMargins = false, dataAttributes, -}) => { +}: ButtonLayoutProps): JSX.Element => { const sortedButtons = React.Children.toArray(children as any).sort((b1: any, b2: any) => { const range1 = buttonsRange.indexOf(b1.type); const range2 = buttonsRange.indexOf(b2.type); diff --git a/src/button.css.ts b/src/button.css.ts index 270e4cd6c1..6800c99b5f 100644 --- a/src/button.css.ts +++ b/src/button.css.ts @@ -1,27 +1,50 @@ -import {style, globalStyle, styleVariants} from '@vanilla-extract/css'; +import {style, globalStyle, styleVariants, createVar} from '@vanilla-extract/css'; import {sprinkles} from './sprinkles.css'; import {vars} from './skins/skin-contract.css'; import * as mq from './media-queries.css'; +import {pxToRem} from './utils/css'; import type {ComplexStyleRule} from '@vanilla-extract/css'; +const minWidth = createVar(); +export const buttonVars = {minWidth}; + const colorTransitionTiming = '0.1s ease-in-out'; const contentTransitionTiming = '0.3s cubic-bezier(0.77, 0, 0.175, 1)'; -export const BUTTON_MIN_WIDTH = 104; -const BORDER_PX = 1.5; -export const ICON_MARGIN_PX = 8; -export const X_PADDING_PX = 16 - BORDER_PX; -const Y_PADDING_PX = 12 - BORDER_PX; -export const X_SMALL_PADDING_PX = 12 - BORDER_PX; -const Y_SMALL_PADDING_PX = 6 - BORDER_PX; -export const ICON_SIZE = 24; -export const SMALL_ICON_SIZE = 20; -export const SPINNER_SIZE = 20; -export const SMALL_SPINNER_SIZE = 16; -export const PADDING_Y_LINK = 6; -export const PADDING_X_LINK = 12; -export const CHEVRON_MARGIN_LEFT_LINK = 2; +export const buttonMinWidth = { + default: '104px', + small: '80px', +}; + +export const linkMinWidth = { + default: '40px', + small: '40px', +}; + +export const borderSize = '1.5px'; +export const iconMargin = '8px'; +export const chevronMarginLeft = '2px'; + +export const iconSize = { + default: pxToRem(24), + small: pxToRem(20), +}; + +export const spinnerSize = { + default: pxToRem(20), + small: pxToRem(16), +}; + +export const buttonPaddingX = { + default: `calc(16px - ${borderSize})`, + small: `calc(12px - ${borderSize})`, +}; + +export const buttonPaddingY = { + default: `calc(12px - ${borderSize})`, + small: `calc(6px - ${borderSize})`, +}; const disabledStyle = {opacity: 0.5}; @@ -37,8 +60,8 @@ const button = style([ padding: 0, }), { - border: `${BORDER_PX}px solid transparent`, - minWidth: BUTTON_MIN_WIDTH, + minWidth: buttonVars.minWidth, + border: `${borderSize} solid transparent`, transition: `background-color ${colorTransitionTiming}, color ${colorTransitionTiming}, border-color ${colorTransitionTiming}`, selectors: { @@ -52,6 +75,15 @@ const button = style([ }, ]); +const link = style([ + button, + { + fontWeight: 500, + }, +]); + +export const small = style({}); + export const loadingFiller = style([ sprinkles({ display: 'block', @@ -63,10 +95,6 @@ export const loadingFiller = style([ }, ]); -export const small = style({ - minWidth: 80, -}); - export const loadingContent = style([ sprinkles({ display: 'inline-flex', @@ -77,16 +105,16 @@ export const loadingContent = style([ alignItems: 'center', }), { - left: X_PADDING_PX, - right: X_PADDING_PX, + left: buttonPaddingX.default, + right: buttonPaddingX.default, opacity: 0, transform: 'translateY(2rem)', transition: `opacity ${contentTransitionTiming}, transform ${contentTransitionTiming}`, selectors: { [`${small} &`]: { - left: X_SMALL_PADDING_PX, - right: X_SMALL_PADDING_PX, + left: buttonPaddingX.small, + right: buttonPaddingX.small, }, [`${isLoading} &`]: { transform: 'translateY(0)', @@ -103,13 +131,13 @@ export const textContent = style([ justifyContent: 'center', }), { - padding: `${Y_PADDING_PX}px ${X_PADDING_PX}px`, // height 48 + padding: `${buttonPaddingY.default} ${buttonPaddingX.default}`, // height 48 opacity: 1, transition: `opacity ${contentTransitionTiming}, transform ${contentTransitionTiming}`, selectors: { [`${small} &`]: { - padding: `${Y_SMALL_PADDING_PX}px ${X_SMALL_PADDING_PX}px`, // height 32 + padding: `${buttonPaddingY.small} ${buttonPaddingX.small}`, // height 32 }, [`${isLoading} &`]: { transform: 'translateY(-2rem)', @@ -282,58 +310,6 @@ const danger: ComplexStyleRule = [ }, ]; -const link = style([ - sprinkles({ - display: 'inline-block', - width: 'auto', - position: 'relative', - borderRadius: vars.borderRadii.button, - paddingX: PADDING_X_LINK, - border: 'none', - overflow: 'hidden', - minWidth: 40, - }), - { - paddingTop: PADDING_Y_LINK, - paddingBottom: PADDING_Y_LINK, - fontWeight: 500, - transition: `background-color ${colorTransitionTiming}`, - - selectors: { - [`&[disabled]:not(${isLoading})`]: disabledStyle, - }, - - '@media': { - [mq.touchableOnly]: { - transition: 'none', - }, - }, - }, -]); - -export const textContentLink = style([ - sprinkles({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }), - { - opacity: 1, - transition: `opacity ${contentTransitionTiming}, transform ${contentTransitionTiming}`, - - selectors: { - [`${isLoading} &`]: { - transform: 'translateY(-2rem)', - opacity: 0, - }, - }, - }, -]); - -globalStyle(`${textContentLink} svg`, { - display: 'block', -}); - export const defaultLink: ComplexStyleRule = [ link, sprinkles({ @@ -478,22 +454,16 @@ export const buttonVariants = styleVariants({ primary: lightPrimary, secondary: lightSecondary, danger, + link: defaultLink, + linkDanger: dangerLink, + linkDangerDark: dangerLink, }); export const inverseButtonVariants = styleVariants({ primary: lightPrimaryInverse, secondary: lightSecondaryInverse, danger, -}); - -export const linkVariants = styleVariants({ - default: defaultLink, - danger: dangerLink, - dangerDark: dangerLink, -}); - -export const inverseLinkVariants = styleVariants({ - default: defaultLinkInverse, - danger: dangerLinkInverse, - dangerDark: dangerLinkInverseDark, + link: defaultLinkInverse, + linkDanger: dangerLinkInverse, + linkDangerDark: dangerLinkInverseDark, }); diff --git a/src/button.tsx b/src/button.tsx index 13b6ac3d7c..af007dee7d 100644 --- a/src/button.tsx +++ b/src/button.tsx @@ -5,7 +5,7 @@ import Spinner from './spinner'; import {BaseTouchable} from './touchable'; import {useIsInverseVariant} from './theme-variant-context'; import {useForm} from './form-context'; -import {pxToRem} from './utils/css'; +import {applyCssVars, pxToRem} from './utils/css'; import {Text, Text2, Text3} from './text'; import Box from './box'; import {getTextFromChildren} from './utils/common'; @@ -27,13 +27,15 @@ import type {Location} from 'history'; import type {ExclusifyUnion} from './utils/utility-types'; const renderButtonElement = ({ + small, content, defaultIconSize, - renderText, + TextContentRenderer, }: { + small?: boolean; content: React.ReactNode; - defaultIconSize: number; - renderText: (text: React.ReactNode) => React.ReactNode; + defaultIconSize: string; + TextContentRenderer: (element: React.ReactNode, small?: boolean) => JSX.Element; }): React.ReactNode => { const childrenArr = flattenChildren(content); const length = childrenArr.length; @@ -41,7 +43,9 @@ const renderButtonElement = ({ let accText: Array = []; const flushAccText = () => { resultChildrenArr.push( - {renderText(accText)} + + {TextContentRenderer(accText, small)} + ); accText = []; }; @@ -56,19 +60,19 @@ const renderButtonElement = ({ if (accText.length) { flushAccText(); } - const sizeInPx = element.props.size ?? defaultIconSize; + const sizeInPx = element.props.size !== undefined ? pxToRem(element.props.size) : defaultIconSize; resultChildrenArr.push(
{React.cloneElement(element as React.ReactElement, { - size: pxToRem(sizeInPx), + size: sizeInPx, })}
); @@ -82,7 +86,7 @@ const renderButtonElement = ({ return resultChildrenArr; }; -const ButtonLinkChevron: React.FC = () => { +const ButtonLinkChevron = () => { const {skinName} = useTheme(); // vivo new skin has a different chevron @@ -114,8 +118,7 @@ const renderButtonContent = ({ loadingText, shouldRenderSpinner, setShouldRenderSpinner, - renderText, - textContentStyle, + TextContentRenderer, StartIcon, EndIcon, withChevron, @@ -126,42 +129,51 @@ const renderButtonContent = ({ loadingText?: string; shouldRenderSpinner: boolean; setShouldRenderSpinner: (value: boolean) => void; - renderText: (text: React.ReactNode) => React.ReactNode; - textContentStyle?: string; - StartIcon?: React.FC; - EndIcon?: React.FC; + TextContentRenderer: (element: React.ReactNode, small?: boolean) => JSX.Element; + StartIcon?: (props: IconProps) => JSX.Element; + EndIcon?: (props: IconProps) => JSX.Element; withChevron?: boolean; }): React.ReactNode => { - const defaultIconSize = small ? styles.SMALL_ICON_SIZE : styles.ICON_SIZE; - const spinnerSizeRem = pxToRem(small ? styles.SMALL_SPINNER_SIZE : styles.SPINNER_SIZE); + const defaultIconSize = small ? styles.iconSize.small : styles.iconSize.default; + const spinnerSizeRem = small ? styles.spinnerSize.small : styles.spinnerSize.default; + + const buttonElement = renderButtonElement({ + small, + content: children, + defaultIconSize, + TextContentRenderer, + }); + + const loadingButtonElement = renderButtonElement({ + small, + content: loadingText, + defaultIconSize, + TextContentRenderer, + }); return ( <> {/* text content */} -
+
{StartIcon && (
- +
)}
- {renderButtonElement({ - content: children, - defaultIconSize, - renderText, - })} + {buttonElement} {withChevron && (
@@ -173,10 +185,10 @@ const renderButtonContent = ({ style={{ display: 'flex', alignItems: 'center', - marginLeft: styles.ICON_MARGIN_PX, + marginLeft: styles.iconMargin, }} > - +
)}
@@ -189,14 +201,12 @@ const renderButtonContent = ({ loadingText ? { paddingLeft: spinnerSizeRem, - paddingRight: - styles.ICON_MARGIN_PX + - 2 * (small ? styles.X_SMALL_PADDING_PX : styles.X_PADDING_PX), + paddingRight: `calc(${styles.iconMargin} + 2 * ${small ? styles.buttonPaddingX.small : styles.buttonPaddingX.default})`, } : undefined } > - {renderButtonElement({content: loadingText, defaultIconSize, renderText})} + {loadingButtonElement}
{/* loading content */} @@ -225,17 +235,13 @@ const renderButtonContent = ({ }} /> )} - {loadingText ? ( - - {renderButtonElement({content: loadingText, defaultIconSize, renderText})} - - ) : null} + {loadingText ? {loadingButtonElement} : null}
); }; -type ButtonType = 'primary' | 'secondary' | 'danger'; +type ButtonType = 'primary' | 'secondary' | 'danger' | 'link' | 'linkDanger'; interface CommonProps { children: React.ReactNode; @@ -254,50 +260,65 @@ interface CommonProps { 'aria-expanded'?: 'true' | 'false' | boolean; 'aria-haspopup'?: 'true' | 'false' | 'menu' | 'dialog' | boolean; tabIndex?: number; - StartIcon?: React.FC; - EndIcon?: React.FC; + StartIcon?: (props: IconProps) => JSX.Element; + EndIcon?: (props: IconProps) => JSX.Element; /** IMPORTANT: try to avoid using role="link" with onPress and first consider other alternatives like to/href + onNavigate */ role?: string; } -export interface ToButtonProps extends CommonProps { +interface ToButtonProps extends CommonProps { to: string | Location; newTab?: boolean; fullPageOnWebView?: boolean; onNavigate?: () => void | Promise; } -export interface OnPressButtonProps extends CommonProps { +interface OnPressButtonProps extends CommonProps { onPress: (event: React.MouseEvent) => void | undefined | Promise; } -export interface HrefButtonProps extends CommonProps { +interface HrefButtonProps extends CommonProps { href: string; newTab?: boolean; loadOnTop?: boolean; - onNavigate?: () => void | Promise; } -export interface FakeButtonProps extends CommonProps { +interface FakeButtonProps extends CommonProps { fake: true; } -export interface SubmitButtonProps extends CommonProps { +interface SubmitButtonProps extends CommonProps { submit: true; } -export type ButtonProps = ExclusifyUnion< +type ButtonProps = ExclusifyUnion< FakeButtonProps | SubmitButtonProps | ToButtonProps | OnPressButtonProps | HrefButtonProps >; -const Button = React.forwardRef((props, ref) => { - const {textPresets} = useTheme(); +type ButtonLinkProps = ExclusifyUnion & { + bleedLeft?: boolean; + bleedRight?: boolean; + bleedY?: boolean; + small?: true; +}; + +const BaseButton = React.forwardRef< + TouchableElement, + ExclusifyUnion & { + buttonType: ButtonType; + withChevron?: boolean; + TextContentRenderer: (element: React.ReactNode, small?: boolean) => JSX.Element; + } +>((props, ref) => { const {eventFormat} = useTrackingConfig(); const {formStatus, formId} = useForm(); const isInverse = useIsInverseVariant(); const {loadingText} = props; const isSubmitButton = !!props.submit; const isFormSending = formStatus === 'sending'; + const {isDarkMode} = useTheme(); const [isOnPressPromiseResolving, setIsOnPressPromiseResolving] = React.useState(false); const showSpinner = props.showSpinner || (isFormSending && isSubmitButton) || isOnPressPromiseResolving; + const showChevron = + props.withChevron ?? (props.buttonType.startsWith('link') && (!!props.href || !!props.to)); // This state is needed to not render the spinner when hidden (because it causes high CPU usage // specially in iPhone). But we want the spinner to be visible during the show/hide animation. @@ -312,50 +333,82 @@ const Button = React.forwardRef { + let component_type; + let action; + + switch (props.buttonType) { + case 'link': + component_type = 'link'; + action = eventActions.linkTapped; + break; + case 'linkDanger': + component_type = 'danger_link'; + action = eventActions.linkTapped; + break; + default: + component_type = `${props.buttonType}_button`; + action = `${props.buttonType}_button_tapped`; + break; + } + if (eventFormat === 'google-analytics-4') { return { name: eventNames.userInteraction, - component_type: `${props.type}_button`, + component_type, component_copy: getTextFromChildren(props.children), }; } else { return { category: eventCategories.userInteraction, - action: `${props.type}_button_tapped`, + action, label: getTextFromChildren(props.children), }; } }; - const renderText = (element: React.ReactNode) => - props.small ? ( - - {element} - - ) : ( - - {element} - - ); + const minWidthProps = props.buttonType.startsWith('link') ? styles.linkMinWidth : styles.buttonMinWidth; + const finalType = + props.buttonType === 'linkDanger' && isDarkMode && isInverse ? 'linkDangerDark' : props.buttonType; const commonProps = { ref, className: classnames( - isInverse ? styles.inverseButtonVariants[props.type] : styles.buttonVariants[props.type], + isInverse ? styles.inverseButtonVariants[finalType] : styles.buttonVariants[finalType], props.className, { [styles.small]: props.small, [styles.isLoading]: showSpinner, } ), - style: {cursor: props.fake ? 'pointer' : undefined, ...props.style}, + style: { + ...applyCssVars({ + [styles.buttonVars.minWidth]: props.small ? minWidthProps.small : minWidthProps.default, + }), + + /** + * Setting bleed classes with style to override the margin:0 set by the Touchable component. + * If we set it using className, it may not work depending on the order in which the styles are applied. + */ + ...(props.bleedLeft + ? { + marginLeft: `calc(-1 * (${styles.borderSize} + ${props.small ? styles.buttonPaddingX.small : styles.buttonPaddingX.default}))`, + } + : undefined), + ...(props.bleedRight + ? { + marginRight: `calc(-1 * (${styles.borderSize} + ${props.small ? styles.buttonPaddingX.small : styles.buttonPaddingX.default}))`, + } + : undefined), + ...(props.bleedY + ? { + marginTop: `calc(-1 * (${styles.borderSize} + ${props.small ? styles.buttonPaddingY.small : styles.buttonPaddingY.default}))`, + marginBottom: `calc(-1 * (${styles.borderSize} + ${props.small ? styles.buttonPaddingY.small : styles.buttonPaddingY.default}))`, + } + : undefined), + + cursor: props.fake ? 'pointer' : undefined, + ...props.style, + }, trackingEvent: props.trackingEvent ?? (props.trackEvent ? createDefaultTrackingEvent() : undefined), dataAttributes: props.dataAttributes, 'aria-label': props['aria-label'], @@ -370,10 +423,10 @@ const Button = React.forwardRef; - trackEvent?: boolean; - /** "data-" prefix is automatically added. For example, use "testid" instead of "data-testid" */ - dataAttributes?: DataAttributes; - showSpinner?: boolean; - loadingText?: string; - StartIcon?: React.FC; - EndIcon?: React.FC; - bleedLeft?: boolean; - bleedRight?: boolean; - bleedY?: boolean; - 'aria-label'?: string; - 'aria-controls'?: string; - 'aria-expanded'?: 'true' | 'false' | boolean; - 'aria-haspopup'?: 'true' | 'false' | 'menu' | 'dialog' | boolean; - /** IMPORTANT: try to avoid using role="link" with onPress and first consider other alternatives like to/href + onNavigate */ - role?: string; -} - -interface ButtonLinkOnPressProps extends ButtonLinkCommonProps { - onPress: (event: React.MouseEvent) => void | undefined | Promise; - to?: undefined; - href?: undefined; - onNavigate?: undefined; -} - -interface ButtonLinkHrefProps extends ButtonLinkCommonProps { - href: string; - newTab?: boolean; - onPress?: undefined; - to?: undefined; - onNavigate?: () => void | Promise; -} - -interface ButtonLinkToProps extends ButtonLinkCommonProps { - to: string; - newTab?: boolean; - fullPageOnWebView?: boolean; - onPress?: undefined; - href?: undefined; - onNavigate?: () => void | Promise; -} - -export type ButtonLinkProps = ButtonLinkOnPressProps | ButtonLinkHrefProps | ButtonLinkToProps; - -const BaseButtonLink = React.forwardRef< - TouchableElement, - ButtonLinkProps & {type: ButtonLinkType; withChevron?: boolean} ->(({type, ...props}, ref) => { - const {formStatus} = useForm(); - const isInverse = useIsInverseVariant(); +const ButtonTextRenderer = (element: React.ReactNode, small?: boolean): JSX.Element => { const {textPresets} = useTheme(); - const {eventFormat} = useTrackingConfig(); - const {isDarkMode} = useTheme(); - - const {loadingText} = props; - const isFormSending = formStatus === 'sending'; - const [isOnPressPromiseResolving, setIsOnPressPromiseResolving] = React.useState(false); - - const showSpinner = props.showSpinner || isOnPressPromiseResolving; - const showChevron = props.withChevron ?? (!!props.href || !!props.to); - - // This state is needed to not render the spinner when hidden (because it causes high CPU usage - // specially in iPhone). But we want the spinner to be visible during the show/hide animation. - // * When showSpinner prop is true, state is changed immediately. - // * When the transition ends this state is updated again if needed - const [shouldRenderSpinner, setShouldRenderSpinner] = React.useState(!!showSpinner); - - React.useEffect(() => { - if (showSpinner && !shouldRenderSpinner) { - setShouldRenderSpinner(true); - } - }, [showSpinner, shouldRenderSpinner, formStatus]); - - const createDefaultTrackingEvent = (): TrackingEvent => { - if (eventFormat === 'google-analytics-4') { - return { - name: eventNames.userInteraction, - component_type: type === 'danger' ? 'danger_link' : 'link', - component_copy: getTextFromChildren(props.children), - }; - } else { - return { - category: eventCategories.userInteraction, - action: eventActions.linkTapped, - label: getTextFromChildren(props.children), - }; - } - }; - - const renderText = (element: React.ReactNode) => ( - + return small ? ( + {element} - + + ) : ( + + {element} + ); +}; - const finalType = type === 'danger' && isDarkMode && isInverse ? 'dangerDark' : type; - - const commonProps = { - className: classnames( - isInverse ? styles.inverseLinkVariants[finalType] : styles.linkVariants[finalType], - { - [styles.isLoading]: showSpinner, - } - ), - /** - * Setting bleed classes with style to override the margin:0 set by the Touchable component. - * If we set it using className, it may not work depending on the order in which the styles are applied. - */ - style: { - ...(props.bleedLeft ? {marginLeft: -styles.PADDING_X_LINK} : undefined), - ...(props.bleedRight ? {marginRight: -styles.PADDING_X_LINK} : undefined), - ...(props.bleedY - ? {marginTop: -styles.PADDING_Y_LINK, marginBottom: -styles.PADDING_Y_LINK} - : undefined), - }, - trackingEvent: props.trackingEvent ?? (props.trackEvent ? createDefaultTrackingEvent() : undefined), - dataAttributes: props.dataAttributes, - 'aria-label': props['aria-label'], - 'aria-controls': props['aria-controls'], - 'aria-expanded': props['aria-expanded'], - 'aria-haspopup': props['aria-haspopup'], - children: renderButtonContent({ - showSpinner, - shouldRenderSpinner, - setShouldRenderSpinner, - children: props.children, - loadingText, - small: true, - renderText, - textContentStyle: styles.textContentLink, - StartIcon: props.StartIcon, - EndIcon: props.EndIcon, - withChevron: showChevron, - }), - disabled: props.disabled || showSpinner || isFormSending, - role: props.role, - }; - - if (process.env.NODE_ENV !== 'production') { - if (props.to === '' || props.href === '') { - throw Error('to or href props are empty strings'); - } - } - - if (props.onPress) { - return ( - { - const result = props.onPress(e); - if (result) { - setIsOnPressPromiseResolving(true); - result.finally(() => setIsOnPressPromiseResolving(false)); - } - }} - /> - ); - } - - if (props.to || props.to === '') { - return ( - - ); - } - - if (props.href || props.href === '') { - return ( - - ); - } - - if (process.env.NODE_ENV !== 'production') { - // this cannot happen - throw Error('Bad button props'); - } - - return null; -}); +const LinkTextRenderer = (element: React.ReactNode, small?: boolean): JSX.Element => { + const {textPresets} = useTheme(); + const TextComponent = small ? Text2 : Text3; + return ( + + {element} + + ); +}; export const ButtonLink = React.forwardRef< TouchableElement, ButtonLinkProps & { withChevron?: boolean; } ->(({dataAttributes, ...props}, ref) => { +>(({dataAttributes, small, ...props}, ref) => { return ( - ); }); export const ButtonLinkDanger = React.forwardRef( - ({dataAttributes, ...props}, ref) => { + ({dataAttributes, small, ...props}, ref) => { return ( - ); } @@ -673,11 +561,12 @@ export const ButtonLinkDanger = React.forwardRef( ({dataAttributes, ...props}, ref) => { return ( -