diff --git a/next-app/package.json b/next-app/package.json index 41e4eb4f..e4df9935 100644 --- a/next-app/package.json +++ b/next-app/package.json @@ -30,7 +30,7 @@ "typescript": "4.8.2" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" } } diff --git a/package.json b/package.json index ba79bbe4..d62cc9ae 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "find-up": "^6.3.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@changesets/changelog-github": "0.4.7", diff --git a/packages/accordion/package.json b/packages/accordion/package.json index 20b8740b..1f7201a1 100644 --- a/packages/accordion/package.json +++ b/packages/accordion/package.json @@ -41,7 +41,7 @@ "@react-aria/interactions": "^3.12.0", "nanoid": "^3.3.4", "react-transition-group": "^4.4.5", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -55,8 +55,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0", + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0", "styled-components": ">= 5.3.6" }, "publishConfig": { diff --git a/packages/active-zone/package.json b/packages/active-zone/package.json index 7b3bc6fa..e86b4ba9 100644 --- a/packages/active-zone/package.json +++ b/packages/active-zone/package.json @@ -39,7 +39,7 @@ "@react-aria/focus": "^3.9.0", "@react-aria/interactions": "^3.12.0", "@react-spectrum/utils": "^3.7.4", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@jengaui/tsconfig": "workspace:0.3.0", @@ -51,8 +51,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/alert-dialog/package.json b/packages/alert-dialog/package.json index 0376efb6..e2d4da5c 100644 --- a/packages/alert-dialog/package.json +++ b/packages/alert-dialog/package.json @@ -40,7 +40,7 @@ "@jengaui/portal": "workspace:0.4.0", "@react-aria/utils": "^3.14.0", "@react-types/dialog": "^3.4.4", - "tastycss": "^0.13.0", + "tastycss": "^0.17.2", "tiny-invariant": "^1.3.1" }, "devDependencies": { @@ -54,8 +54,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/alert/package.json b/packages/alert/package.json index 201185eb..5a8ce292 100644 --- a/packages/alert/package.json +++ b/packages/alert/package.json @@ -33,7 +33,7 @@ "dependencies": { "@jengaui/core": "workspace:0.4.0", "@jengaui/hooks": "workspace:0.4.0", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@jengaui/tsconfig": "workspace:0.3.0", @@ -45,8 +45,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/avatar/package.json b/packages/avatar/package.json index dca2104a..8440733f 100644 --- a/packages/avatar/package.json +++ b/packages/avatar/package.json @@ -33,7 +33,7 @@ "lint": "TIMING=1 eslint src/**/*.ts* --fix" }, "dependencies": { - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -46,8 +46,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/badge/package.json b/packages/badge/package.json index 1f30bfe3..0c39f49f 100644 --- a/packages/badge/package.json +++ b/packages/badge/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@jengaui/core": "workspace:0.4.0", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -47,8 +47,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/banner/package.json b/packages/banner/package.json index 3b716ab5..43dee34a 100644 --- a/packages/banner/package.json +++ b/packages/banner/package.json @@ -42,7 +42,7 @@ "@jengaui/button": "workspace:0.4.0", "@jengaui/card": "workspace:0.4.0", "@jengaui/core": "workspace:0.4.0", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -55,8 +55,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/breadcrumbs/package.json b/packages/breadcrumbs/package.json index ff644d0c..4b0f6dea 100644 --- a/packages/breadcrumbs/package.json +++ b/packages/breadcrumbs/package.json @@ -38,7 +38,7 @@ "@react-aria/breadcrumbs": "^3.3.2", "@react-types/breadcrumbs": "^3.4.4", "@react-types/shared": "3.15.0", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -51,8 +51,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/button-group/package.json b/packages/button-group/package.json index afa085fa..6fb732e3 100644 --- a/packages/button-group/package.json +++ b/packages/button-group/package.json @@ -36,7 +36,7 @@ "dependencies": { "@jengaui/layout": "workspace:0.4.0", "@jengaui/utils": "workspace:0.4.0", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { "@ant-design/icons": "^4.7.0", @@ -49,8 +49,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/button-group/src/ButtonGroup.tsx b/packages/button-group/src/ButtonGroup.tsx index fc4883c1..ddde8386 100644 --- a/packages/button-group/src/ButtonGroup.tsx +++ b/packages/button-group/src/ButtonGroup.tsx @@ -18,4 +18,4 @@ export const ButtonGroup = forwardRef(function ButtonGroup( return ( ); -}); +}); \ No newline at end of file diff --git a/packages/button/package.json b/packages/button/package.json index ce641c29..cc14392a 100644 --- a/packages/button/package.json +++ b/packages/button/package.json @@ -36,12 +36,13 @@ "@jengaui/form": "workspace:0.4.0", "@jengaui/providers": "workspace:0.4.0", "@jengaui/utils": "workspace:0.4.0", + "@jengaui/hooks": "workspace:0.4.0", "@react-aria/button": "^3.6.2", "@react-aria/interactions": "^3.12.0", "@react-spectrum/utils": "^3.7.4", "@react-types/shared": "^3.15.0", "react-is": "^17.0.2", - "tastycss": "^0.13.0" + "tastycss": "^0.17.2" }, "devDependencies": { @@ -58,8 +59,8 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "react": ">= 18.2.0", - "react-dom": ">= 18.2.0" + "react": ">= 17.0.0", + "react-dom": ">= 17.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/button/src/Action.tsx b/packages/button/src/Action.tsx index 19317309..dc254847 100644 --- a/packages/button/src/Action.tsx +++ b/packages/button/src/Action.tsx @@ -1,26 +1,22 @@ -import { forwardRef, MouseEventHandler, useCallback, useContext } from 'react'; -import { useHover } from '@react-aria/interactions'; -import { useButton } from '@react-aria/button'; +import { forwardRef, MouseEventHandler } from 'react'; import { AriaButtonProps } from '@react-types/button'; -import { useFocusableRef } from '@react-spectrum/utils'; import { FocusableRef } from '@react-types/shared'; -import { UIKitContext } from '@jengaui/providers'; -import { mergeProps } from '@jengaui/utils'; -import { useFocus } from '@jengaui/utils'; + import { BaseProps, BaseStyleProps, CONTAINER_STYLES, ContainerStyleProps, extractStyles, - filterBaseProps, Styles, TagNameProps, TEXT_STYLES, TextStyleProps, - Element, + tasty, } from 'tastycss'; +import { useAction } from './use-action'; + export interface JengaActionProps extends BaseProps, TagNameProps, @@ -36,93 +32,11 @@ export interface JengaActionProps onMouseLeave?: MouseEventHandler; } -const FILTER_OPTIONS = { propNames: new Set(['onMouseEnter', 'onMouseLeave']) }; - -/** - * Helper to open link. - * @param {String} href - * @param {String|Boolean} [target] - */ -export function openLink(href, target?) { - const link = document.createElement('a'); - - link.href = href; - - if (target) { - link.target = target === true ? '_blank' : target; - } - - document.body.appendChild(link); - - link.click(); - - document.body.removeChild(link); -} - -export function parseTo(to): { - newTab: boolean; - nativeRoute: boolean; - href: string | undefined; -} { - const newTab = to && typeof to === 'string' && to.startsWith('!'); - const nativeRoute = to && typeof to === 'string' && to.startsWith('@'); - const href: string | undefined = - to && typeof to === 'string' - ? newTab || nativeRoute - ? to.slice(1) - : to - : undefined; - - return { - newTab, - nativeRoute, - href, - }; -} - -export function performClickHandler(evt, router, to, onPress) { - const { newTab, nativeRoute, href } = parseTo(to); - - onPress?.(evt); - - if (!to) return; - - if (evt.shiftKey || evt.metaKey || newTab) { - openLink(href, true); - - return; - } - - if (nativeRoute) { - openLink(href || window.location.href); - } else if (href && href.startsWith('#')) { - const id = href.slice(1); - const element = document.getElementById(id); - - if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest', - }); - - return; - } - } - - if (router) { - router.push(href); - } else if (href) { - window.location.href = href; - } -} - -const DEFAULT_STYLES: Styles = { +const DEFAULT_ACTION_STYLES: Styles = { reset: 'button', position: 'relative', margin: 0, - fontFamily: 'var(--font)', - fontWeight: 'inherit', + preset: 'inherit', border: 0, padding: 0, outline: { @@ -131,74 +45,27 @@ const DEFAULT_STYLES: Styles = { }, transition: 'theme', cursor: 'pointer', + textDecoration: 'none', + fill: '#clear', } as const; +const ActionElement = tasty({ + as: 'button', + styles: DEFAULT_ACTION_STYLES, +}); + const STYLE_PROPS = [...CONTAINER_STYLES, ...TEXT_STYLES]; export const Action = forwardRef(function Action( { to, as, htmlType, label, theme, mods, onPress, ...props }: JengaActionProps, ref: FocusableRef, ) { - as = to ? 'a' : as || 'button'; - - const router = useContext(UIKitContext).router; - const isDisabled = props.isDisabled; - const { newTab, href } = parseTo(to); - const target = newTab ? '_blank' : undefined; - const domRef = useFocusableRef(ref); - const styles = extractStyles(props, STYLE_PROPS, DEFAULT_STYLES); - - const customOnPress = useCallback( - (evt) => { - performClickHandler(evt, router, to, onPress); - }, - [router, to, onPress], + const { actionProps } = useAction( + { to, as, htmlType, label, onPress, mods, ...props }, + ref, ); - let { buttonProps, isPressed } = useButton( - { - ...props, - onPress: customOnPress, - }, - domRef, - ); - let { hoverProps, isHovered } = useHover({ isDisabled }); - let { focusProps, isFocused } = useFocus({ isDisabled }, true); + const styles = extractStyles(props, STYLE_PROPS); - const customProps = to - ? { - onClick(evt) { - evt.preventDefault(); - }, - } - : {}; - - return ( - - ); -}); + return ; +}); \ No newline at end of file diff --git a/packages/button/src/Button.tsx b/packages/button/src/Button.tsx index 6216deaf..5b2535c1 100644 --- a/packages/button/src/Button.tsx +++ b/packages/button/src/Button.tsx @@ -1,16 +1,21 @@ import { cloneElement, forwardRef, ReactElement, useMemo } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import { FocusableRef } from '@react-types/shared'; -import { Styles } from 'tastycss'; -import { accessibilityWarning } from '@jengaui/utils'; -import type {} from 'csstype'; -import { Action, JengaActionProps } from './Action'; +import { JengaActionProps } from './Action'; +import { + CONTAINER_STYLES, + extractStyles, + Styles, + tasty, + TEXT_STYLES, +} from 'tastycss'; +import { accessibilityWarning } from '@jengaui/utils'; +import { useAction } from './use-action'; export interface JengaButtonProps extends JengaActionProps { icon?: ReactElement; rightIcon?: ReactElement; - isDisabled?: boolean; isLoading?: boolean; isSelected?: boolean; type?: @@ -25,258 +30,241 @@ export interface JengaButtonProps extends JengaActionProps { size?: 'small' | 'medium' | 'large' | (string & {}); } -export function provideButtonStyles({ type, theme }) { - return { - ...(theme === 'danger' ? DANGER_STYLES_BY_TYPE : DEFAULT_STYLES_BY_TYPE)[ - type ?? 'secondary' - ], - }; -} - -const DEFAULT_STYLES_BY_TYPE: { [key: string]: Styles } = { - primary: { - border: { - '': '#clear', - pressed: '#purple-text', - }, - fill: { - hovered: '#purple-text', - 'pressed | !hovered': '#purple', - '[disabled]': '#dark.04', - }, - color: { - '': '#white', - '[disabled]': '#dark.30', - }, - }, - secondary: { - border: { - '': '#clear', - pressed: '#purple.30', - }, - fill: { - '': '#purple.10', - hovered: '#purple.16', - pressed: '#purple.10', - '[disabled]': '#dark.04', - }, - color: { - '': '#purple', - '[disabled]': '#dark.30', - }, - }, - clear: { - border: { - '': '#clear', - pressed: '#purple-text.10', - }, - fill: { - '': '#purple.0', - hovered: '#purple.16', - pressed: '#purple.10', - '[disabled]': '#purple.0', - }, - color: { - '': '#purple-text', - '[disabled]': '#dark.30', - }, - }, - outline: { - border: { - '': '#purple.30', - pressed: '#purple-text.10', - '[disabled]': '#dark.12', - }, - fill: { - '': '#purple.0', - hovered: '#purple.16', - pressed: '#purple.10', - '[disabled]': '#purple.0', - }, - color: { - '': '#purple-text', - '[disabled]': '#dark.30', - }, - }, - link: { - fontWeight: 500, - padding: '0', - radius: { - '': '0', - focused: true, - }, - fill: '#clear', - color: { - '': '#purple-text', - pressed: '#purple', - '[disabled]': '#dark.30', - }, - shadow: { - '': '0 @border-width 0 0 #purple-03.20', - focused: '0 0 0 @outline-width #purple-03.20', - 'pressed | hovered | [disabled]': '0 0 0 0 #purple.20', - }, - }, - neutral: { - border: '#clear', - fill: { - '': '#dark.0', - hovered: '#dark.04', - pressed: '#purple.10', - '[disabled]': '#dark.04', - }, - color: { - '': '#dark.75', - hovered: '#dark.75', - pressed: '#purple', - '[disabled]': '#dark.30', - }, - }, -}; - -const DANGER_STYLES_BY_TYPE: { [key: string]: Styles } = { - primary: { - border: { - '': '#clear', - pressed: '#danger-text', - }, - fill: { - hovered: '#danger-text', - 'pressed | !hovered': '#danger', - '[disabled]': '#dark.04', - }, - color: { - '': '#white', - '[disabled]': '#dark.30', - }, - }, - secondary: { - border: { - '': '#clear', - pressed: '#danger.30', - }, - fill: { - '': '#danger.05', - hovered: '#danger.1', - pressed: '#danger.05', - '[disabled]': '#dark.04', - }, - color: { - '': '#danger', - '[disabled]': '#dark.30', - }, - }, - clear: { - border: { - '': '#clear', - pressed: '#danger-text.10', - }, - fill: { - '': '#danger.0', - hovered: '#danger.1', - pressed: '#danger.05', - '[disabled]': '#danger.0', - }, - color: { - '': '#danger-text', - '[disabled]': '#dark.30', - }, - }, - outline: { - border: { - '': '#danger.30', - pressed: '#danger-text.10', - '[disabled]': '#dark.04', - }, - fill: { - '': '#danger.0', - hovered: '#danger.1', - pressed: '#danger.05', - '[disabled]': '#danger.0', - }, - color: { - '': '#danger-text', - '[disabled]': '#dark.30', - }, - }, - link: { - ...DEFAULT_STYLES_BY_TYPE.link, - color: { - '': '#danger-text', - pressed: '#danger', - '[disabled]': '#dark.30', - }, - shadow: { - '': '0 @border-width 0 0 #danger.20', - focused: '0 0 0 @outline-width #danger.20', - 'pressed | hovered | [disabled]': '0 0 0 0 #danger.20', - }, - }, - neutral: { - border: '0', - fill: { - '': '#dark.0', - hovered: '#dark.04', - pressed: '#dark.05', - '[disabled]': '#dark.04', - }, - color: { - '': '#dark.75', - hovered: '#dark.75', - pressed: '#danger', - '[disabled]': '#dark.30', - }, - }, -}; +const STYLE_PROPS = [...CONTAINER_STYLES, ...TEXT_STYLES]; -export const DEFAULT_BUTTON_STYLES = { +export const DEFAULT_BUTTON_STYLES: Styles = { display: 'inline-grid', placeItems: 'center stretch', placeContent: 'center', + position: 'relative', + margin: 0, + outline: { + '': '#purple-03.0', + focused: '#purple-03', + }, + cursor: 'pointer', gap: '1x', flow: 'column', - radius: true, - fontWeight: 500, preset: { '': 't3m', '[data-size="large"]': 't2m', }, textDecoration: 'none', transition: 'theme', + reset: 'button', padding: { '': '(1.25x - 1bw) (2x - 1bw)', '[data-size="small"]': '(.75x - 1bw) (1.5x - 1bw)', '[data-size="medium"]': '(1.25x - 1bw) (2x - 1bw)', '[data-size="large"]': '(1.5x - 1bw) (2.5x - 1bw)', - 'single-icon-only': 0, + 'single-icon-only | [data-type="link"]': 0, }, width: { '': 'initial', - '[data-size="small"] & single-icon-only': '4x', - '[data-size="medium"] & single-icon-only': '5x', - '[data-size="large"] & single-icon-only': '6x', + '[data-size="small"] & single-icon-only': '4x 4x', + '[data-size="medium"] & single-icon-only': '5x 5x', + '[data-size="large"] & single-icon-only': '6x 6x', }, height: { '': 'initial', - '[data-size="small"] & single-icon-only': '4x', - '[data-size="medium"] & single-icon-only': '5x', - '[data-size="large"] & single-icon-only': '6x', + '[data-size="small"] & single-icon-only': '4x 4x', + '[data-size="medium"] & single-icon-only': '5x 5x', + '[data-size="large"] & single-icon-only': '6x 6x', }, whiteSpace: 'nowrap', + radius: { + '': true, + '[data-type="link"] & !focused': 0, + }, + '& .anticon': { transition: 'display .2s steps(1, start), margin .2s linear, opacity .2s linear', }, ButtonIcon: { - fontSize: { - '': 'initial', - '[data-size="small"]': '14px', - '[data-size="medium"]': '16px', - '[data-size="large"]': '18px', + display: 'grid', + fontSize: '@icon-size', + }, +}; + +const ButtonElement = tasty({ + qa: 'Button', + styles: DEFAULT_BUTTON_STYLES, + variants: { + default: { + shadow: { + '': false, + '[data-type="link"]': '0 @border-width 0 0 #purple.20', + '[data-type="link"] & (pressed | hovered | [disabled])': + '0 0 0 0 #purple.20', + }, + outline: { + '': '0 #purple-03.0', + focused: '@outline-width #purple-03', + }, + border: { + // default + '': '#clear', + '[data-type="primary"] & pressed': '#purple-text', + '[data-type="secondary"] & pressed': '#purple.3', + '[data-type="outline"]': '#purple.3', + '[data-type="outline"] & [disabled]': '#dark.12', + '([data-type="clear"] | [data-type="outline"]) & pressed': + '#purple-text.10', + '[data-type="link"]': '0', + }, + fill: { + '': '#clear', + + '[data-type="primary"]': '#purple', + '[data-type="primary"] & pressed': '#purple', + '[data-type="primary"] & hovered': '#purple-text', + + '[data-type="secondary"]': '#purple.10', + '[data-type="secondary"] & hovered': '#purple.16', + '[data-type="secondary"] & pressed': '#purple-text.10', + + '[data-type="neutral"]': '#dark.0', + '[data-type="neutral"] & hovered': '#dark.04', + '[data-type="neutral"] & pressed': '#dark.05', + + '[disabled] & ![data-type="link"]': '#dark.04', + + '([data-type="clear"] | [data-type="outline"])': '#purple.0', + '([data-type="clear"] | [data-type="outline"]) & hovered': '#purple.16', + '([data-type="clear"] | [data-type="outline"]) & pressed': '#purple.10', + '([data-type="clear"] | [data-type="outline"]) & [disabled]': + '#purple.0', + }, + color: { + // default + '': '#white', + '[data-type="secondary"]': '#purple', + '[data-type="clear"] | [data-type="outline"] | [data-type="link"]': + '#purple-text', + '[data-type="link"] & pressed': '#purple', + '[data-type="neutral"]': '#dark.75', + '[data-type="neutral"] & hovered': '#dark.75', + '[data-type="neutral"] & pressed': '#purple', + + // other + '[disabled]': '#dark.30', + }, + }, + danger: { + shadow: { + '': false, + '[data-type="link"]': '0 @border-width 0 0 #danger.20', + '[data-type="link"] & (pressed | hovered | [disabled])': + '0 0 0 0 #danger.20', + }, + outline: { + '': '0 #danger.0', + focused: '@outline-width #danger.50', + }, + border: { + '': '#clear', + '[data-type="primary"] & pressed': '#danger-text', + '[data-type="secondary"] & pressed': '#danger.3', + '[data-type="outline"]': '#danger-text.3', + '([data-type="clear"] | [data-type="outline"]) & pressed': + '#danger-text.10', + '[data-type="outline"] & pressed': '#danger.3', + '[data-type="link"]': '#clear', + }, + fill: { + '': '#clear', + '[data-type="primary"]': '#danger-text', + '[data-type="primary"] & hovered': '#danger-text', + '[data-type="primary"] & pressed': '#danger', + + '[data-type="secondary"]': '#danger.05', + '[data-type="secondary"] & hovered': '#danger.1', + '[data-type="secondary"] & pressed': '#danger.05', + + '[data-type="neutral"]': '#dark.0', + '[data-type="neutral"] & hovered': '#dark.04', + '[data-type="neutral"] & pressed': '#dark.05', + + '[disabled] & ![data-type="link"]': '#dark.04', + + '[data-type="clear"] | [data-type="outline"]': '#danger.0', + '([data-type="clear"] | [data-type="outline"]) & hovered': '#danger.1', + '([data-type="clear"] | [data-type="outline"]) & pressed': '#danger.05', + '([data-type="clear"] | [data-type="outline"]) & [disabled]': + '#danger.0', + }, + color: { + '': '#white', + + '[data-type="neutral"]': '#dark.75', + '[data-type="neutral"] & hovered': '#dark.75', + '[data-type="secondary"]': '#danger', + '[data-type="clear"] | [data-type="outline"] | [data-type="link"]': + '#danger-text', + '[data-type="link"] & pressed': '#danger', + '[data-type="neutral"] & pressed': '#danger', + + '[disabled]': '#dark.30', + }, + }, + special: { + shadow: { + '': false, + '[data-type="link"]': '0 @border-width 0 0 #white.44', + '[data-type="link"] & (pressed | hovered | [disabled])': + '0 0 0 0 #white.44', + }, + outline: { + '': '0 #white.0', + focused: '@outline-width #white.44', + '([data-type="primary"] | [data-type="clear"])': '0 #dark-03.80', + '([data-type="primary"] | [data-type="clear"]) & focused': + '@outline-width #purple-03.80', + }, + border: { + '': '#clear', + '[data-type="primary"] & pressed': '#purple-03', + '[data-type="secondary"] & pressed': '#white.44', + '[data-type="outline"] & !pressed': '#white.44', + }, + fill: { + '': '#clear', + + '[data-type="primary"]': '#purple', + '[data-type="primary"] & pressed': '#purple', + '[data-type="primary"] & hovered': '#purple-text', + + '[data-type="secondary"]': '#white.12', + + '[data-type="clear"]': '#white', + '[data-type="clear"] & hovered': '#white.94', + '[data-type="clear"] & pressed': '#white', + + '[disabled] & ![data-type="link"]': '#white.12', + + '([data-type="neutral"] | [data-type="outline"])': '#white.0', + '([data-type="neutral"] | [data-type="outline"] | [data-type="secondary"]) & hovered': + '#white.18', + '([data-type="neutral"] | [data-type="outline"] | [data-type="secondary"]) & pressed': + '#white.12', + + '([data-type="clear"] | [data-type="outline"]) & [disabled]': + '#white.0', + }, + color: { + // default + '': '#white', + + '[data-type="clear"]': '#purple', + + // other + '[disabled]': '#white.30', + }, }, }, -} as Styles; +}); export const Button = forwardRef(function Button( allProps: JengaButtonProps, @@ -286,46 +274,38 @@ export const Button = forwardRef(function Button( type, size, label, - styles, children, - theme, + theme = 'default', icon, rightIcon, mods, ...props } = allProps; - const isDisabled = props.isDisabled; + const isDisabled = props.isDisabled || props.isLoading; const isLoading = props.isLoading; const isSelected = props.isSelected; + children = children || icon || rightIcon ? children : label; + if (!children) { if (icon) { if (!label) { accessibilityWarning( 'If you provide `icon` property for a Button and do not provide any children then you should specify the `label` property to make sure the Button element stays accessible.', ); + label = 'Unnamed'; // fix to avoid warning in production } } else { if (!label) { accessibilityWarning( 'If you provide no children for a Button then you should specify the `label` property to make sure the Button element stays accessible.', ); + label = 'Unnamed'; // fix to avoid warning in production } } } - children = children || icon || rightIcon ? children : label; - - styles = useMemo( - () => ({ - ...DEFAULT_BUTTON_STYLES, - ...provideButtonStyles({ type, theme }), - ...styles, - }), - [type, theme, styles], - ); - if (icon) { icon = cloneElement(icon, { 'data-element': 'ButtonIcon', @@ -345,7 +325,6 @@ export const Button = forwardRef(function Button( const modifiers = useMemo( () => ({ - disabled: isDisabled, loading: isLoading, selected: isSelected, 'single-icon-only': singleIcon, @@ -354,22 +333,25 @@ export const Button = forwardRef(function Button( [mods, isDisabled, isLoading, isSelected, singleIcon], ); + const { actionProps } = useAction( + { ...allProps, isDisabled, mods: modifiers, ...(label ? { label } : {}) }, + ref, + ); + + const styles = extractStyles(props, STYLE_PROPS); + return ( - {icon || isLoading ? !isLoading ? icon : : null} {children} {rightIcon} - + ); -}); +}); \ No newline at end of file diff --git a/packages/button/src/Submit.tsx b/packages/button/src/Submit.tsx index 5636ec2c..9d06e29d 100644 --- a/packages/button/src/Submit.tsx +++ b/packages/button/src/Submit.tsx @@ -28,4 +28,4 @@ function Submit(props, ref) { } const _Submit = forwardRef(Submit); -export { _Submit as Submit }; +export { _Submit as Submit }; \ No newline at end of file diff --git a/packages/button/src/use-action.ts b/packages/button/src/use-action.ts new file mode 100644 index 00000000..8b48f4e5 --- /dev/null +++ b/packages/button/src/use-action.ts @@ -0,0 +1,188 @@ +import { MouseEventHandler, useContext } from 'react'; +import { useHover } from '@react-aria/interactions'; +import { useButton } from '@react-aria/button'; +import { AriaButtonProps } from '@react-types/button'; +import { useFocusableRef } from '@react-spectrum/utils'; +import { FocusableRef, PressEvent } from '@react-types/shared'; + +import { UIKitContext } from '@jengaui/providers'; +import { mergeProps, useFocus } from '@jengaui/utils'; +import { BaseProps, filterBaseProps, TagNameProps } from 'tastycss'; +import { useTracking } from '@jengaui/providers'; +import { useEvent } from '@jengaui/hooks'; + +const LINK_PRESS_EVENT = 'Link Press'; +const BUTTON_PRESS_EVENT = 'Button Press'; + +export interface JengaUseActionProps + extends BaseProps, + TagNameProps, + Omit { + to?: string; + label?: string; + htmlType?: 'button' | 'submit' | 'reset' | undefined; + onClick?: MouseEventHandler; + onMouseEnter?: MouseEventHandler; + onMouseLeave?: MouseEventHandler; +} + +const FILTER_OPTIONS = { propNames: new Set(['onMouseEnter', 'onMouseLeave']) }; + +/** + * Helper to open link. + * @param {String} href + * @param {String|Boolean} [target] + */ +export function openLink(href, target?) { + const link = document.createElement('a'); + + link.href = href; + + if (target) { + link.target = target === true ? '_blank' : target; + } + + document.body.appendChild(link); + + link.click(); + + document.body.removeChild(link); +} + +export function parseTo(to): { + newTab: boolean; + nativeRoute: boolean; + href: string | undefined; +} { + const newTab = to && typeof to === 'string' && to.startsWith('!'); + const nativeRoute = to && typeof to === 'string' && to.startsWith('@'); + const href: string | undefined = + to && typeof to === 'string' + ? newTab || nativeRoute + ? to.slice(1) + : to + : undefined; + + return { + newTab, + nativeRoute, + href, + }; +} + +export function performClickHandler(evt, { router, to, onPress, tracking }) { + const { newTab, nativeRoute, href } = parseTo(to); + const element = evt.target; + const qa = element?.getAttribute('data-qa'); + + onPress?.(evt); + + if (!to) { + tracking.event(BUTTON_PRESS_EVENT, { qa }, element); + + return; + } + + if (evt.shiftKey || evt.metaKey || newTab) { + openLink(href, true); + + tracking.event(LINK_PRESS_EVENT, { qa, href, type: 'tab' }, element); + + return; + } + + if (nativeRoute) { + openLink(href || window.location.href); + tracking.event(LINK_PRESS_EVENT, { qa, href, type: 'native' }, element); + } else if (href && href.startsWith('#')) { + const id = href.slice(1); + const element = document.getElementById(id); + + tracking.event( + LINK_PRESS_EVENT, + { qa, href, type: 'hash', target: id }, + element, + ); + + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + + return; + } + } + + if (router) { + tracking.event(LINK_PRESS_EVENT, { qa, href, type: 'router' }, element); + router.push(href); + } else if (href) { + tracking.event(LINK_PRESS_EVENT, { qa, href, type: 'native' }, element); + window.location.href = href; + } +} + +export const useAction = function useAction( + { to, as, htmlType, label, mods, onPress, ...props }: JengaUseActionProps, + ref: FocusableRef, +) { + as = to ? 'a' : as || 'button'; + + const tracking = useTracking(); + const router = useContext(UIKitContext).router; + const isDisabled = props.isDisabled; + const { newTab, href } = parseTo(to); + const target = newTab ? '_blank' : undefined; + const domRef = useFocusableRef(ref); + + const customOnPress = useEvent((evt: PressEvent) => { + performClickHandler(evt, { router, to, onPress, tracking }); + }); + + let { buttonProps, isPressed } = useButton( + { + 'aria-label': label, + ...props, + onPress: customOnPress, + }, + domRef, + ); + let { hoverProps, isHovered } = useHover({ isDisabled }); + let { focusProps, isFocused } = useFocus({ isDisabled }, true); + + const customProps = to + ? { + onClick(evt) { + evt.preventDefault(); + }, + } + : {}; + + return { + actionProps: { + mods: { + hovered: isHovered && !isDisabled, + pressed: isPressed && !isDisabled, + focused: isFocused && !isDisabled, + disabled: isDisabled, + ...mods, + }, + ...(mergeProps( + buttonProps, + hoverProps, + focusProps, + customProps, + filterBaseProps(props, FILTER_OPTIONS), + ) as object), + ref: domRef, + type: htmlType || 'button', + rel: as === 'a' && newTab ? 'rel="noopener noreferrer"' : undefined, + as, + isDisabled, + target, + href, + }, + }; +}; \ No newline at end of file diff --git a/packages/button/src/utils/index.ts b/packages/button/src/utils/index.ts index 92de38b3..8f95a6cc 100644 --- a/packages/button/src/utils/index.ts +++ b/packages/button/src/utils/index.ts @@ -1 +1 @@ -export * from './mapProps'; +export * from './mapProps'; \ No newline at end of file diff --git a/packages/button/src/utils/mapProps.ts b/packages/button/src/utils/mapProps.ts index b3654fd0..1d422d05 100644 --- a/packages/button/src/utils/mapProps.ts +++ b/packages/button/src/utils/mapProps.ts @@ -24,4 +24,4 @@ export function jengaToAriaButtonProps( ...filteredProps, type: htmlType, }; -} +} \ No newline at end of file diff --git a/packages/button/stories/Button.stories.tsx b/packages/button/stories/Button.stories.tsx index c0681b82..b46a7ace 100644 --- a/packages/button/stories/Button.stories.tsx +++ b/packages/button/stories/Button.stories.tsx @@ -1,7 +1,7 @@ import { CaretDownOutlined, DollarCircleOutlined } from '@ant-design/icons'; import { baseProps } from '../../../storybook/stories/lists/baseProps'; -import { Space } from '../../layout'; +import { Space } from '@jengaui/layout'; import { Button } from '../src/Button'; @@ -31,20 +31,26 @@ export default { }, theme: { defaultValue: undefined, - control: { type: 'radio', options: [undefined, 'danger'] }, + control: { type: 'radio', options: [undefined, 'danger', 'special'] }, }, }, }; const Template = ({ icon, rightIcon, label, onClick, ...props }) => ( - + + ); const TemplateSizes = ({ label, icon, rightIcon, size, ...props }) => ( @@ -136,6 +142,66 @@ const TemplateStates = ({ label, mods, ...props }) => ( ); +const DarkTemplateStates = ({ label, mods, ...props }) => ( + + + + + + + +); + export const Default = Template.bind({}); Default.args = { label: 'Button', @@ -171,6 +237,78 @@ LinkStates.args = { type: 'link', }; +export const DangerSecondaryStates = TemplateStates.bind({}); +DangerSecondaryStates.args = { + type: 'secondary', + theme: 'danger', +}; + +export const DangerPrimaryStates = TemplateStates.bind({}); +DangerPrimaryStates.args = { + type: 'primary', + theme: 'danger', +}; + +export const DangerOutlineStates = TemplateStates.bind({}); +DangerOutlineStates.args = { + type: 'outline', + theme: 'danger', +}; + +export const DangerClearStates = TemplateStates.bind({}); +DangerClearStates.args = { + type: 'clear', + theme: 'danger', +}; + +export const DangerNeutralStates = TemplateStates.bind({}); +DangerNeutralStates.args = { + type: 'neutral', + theme: 'danger', +}; + +export const DangerLinkStates = TemplateStates.bind({}); +DangerLinkStates.args = { + type: 'link', + theme: 'danger', +}; + +export const SpecialSecondaryStates = DarkTemplateStates.bind({}); +SpecialSecondaryStates.args = { + type: 'secondary', + theme: 'special', +}; + +export const SpecialPrimaryStates = DarkTemplateStates.bind({}); +SpecialPrimaryStates.args = { + type: 'primary', + theme: 'special', +}; + +export const SpecialOutlineStates = DarkTemplateStates.bind({}); +SpecialOutlineStates.args = { + type: 'outline', + theme: 'special', +}; + +export const SpecialClearStates = DarkTemplateStates.bind({}); +SpecialClearStates.args = { + type: 'clear', + theme: 'special', +}; + +export const SpecialNeutralStates = DarkTemplateStates.bind({}); +SpecialNeutralStates.args = { + type: 'neutral', + theme: 'special', +}; + +export const SpecialLinkStates = DarkTemplateStates.bind({}); +SpecialLinkStates.args = { + type: 'link', + theme: 'special', +}; + export const Small = Template.bind({}); Small.args = { label: 'Button', @@ -183,12 +321,6 @@ Large.args = { size: 'large', }; -export const Danger = Template.bind({}); -Danger.args = { - label: 'Button', - theme: 'danger', -}; - export const LeftIconAndText = TemplateSizes.bind({}); LeftIconAndText.args = { label: 'Button', @@ -218,4 +350,4 @@ Loading.args = { icon: true, isLoading: true, label: 'Button', -}; +}; \ No newline at end of file diff --git a/packages/button/test/button.test.tsx b/packages/button/test/button.test.tsx index aa45b8b8..a0965589 100644 --- a/packages/button/test/button.test.tsx +++ b/packages/button/test/button.test.tsx @@ -40,4 +40,4 @@ describe('