diff --git a/.husky/commit-msg b/.husky/commit-msg index 0bd658f496..70bd3dd23d 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx --no-install commitlint --edit "$1" diff --git a/package.json b/package.json index a450fa2220..d6ddfb9f93 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "eslint": "^8.57.0", "eslint-config-smarthr": "^6.33.0", "eslint-plugin-storybook": "^0.8.0", - "husky": "^9.0.11", + "husky": "^9.1.1", "lint-staged": "^15.2.7", "prettier": "^3.3.3", "prettier-config-smarthr": "^1.0.0", @@ -27,7 +27,7 @@ "commitmsg": "commitlint -e $GIT_PARAMS", "prepare": "husky" }, - "packageManager": "pnpm@9.5.0", + "packageManager": "pnpm@9.6.0", "pnpm": { "overrides": { "@babel/helper-compilation-targets": "^7.24.8", diff --git a/packages/smarthr-ui/.storybook/preview.tsx b/packages/smarthr-ui/.storybook/preview.tsx index 6e065a84a5..4858bcf222 100644 --- a/packages/smarthr-ui/.storybook/preview.tsx +++ b/packages/smarthr-ui/.storybook/preview.tsx @@ -38,6 +38,7 @@ const preview: Preview = { 'Page Templates(ページテンプレート)', 'States(状態)', 'Text(テキスト)', + 'Hooks', 'Experimental(実験的)', ], }, diff --git a/packages/smarthr-ui/package.json b/packages/smarthr-ui/package.json index 93edd5f54b..0d22d8a0c4 100644 --- a/packages/smarthr-ui/package.json +++ b/packages/smarthr-ui/package.json @@ -6,7 +6,7 @@ "dependencies": { "@smarthr/wareki": "^1.2.0", "css-loader": "^7.1.2", - "dayjs": "^1.11.11", + "dayjs": "^1.11.12", "lodash.merge": "^4.6.2", "lodash.range": "^3.2.0", "polished": "^4.3.0", @@ -17,38 +17,38 @@ "react-transition-group": "^4.4.5", "style-loader": "^4.0.0", "tailwind-variants": "^0.2.1", - "tailwindcss": "^3.4.4" + "tailwindcss": "^3.4.6" }, "devDependencies": { - "@babel/core": "^7.24.8", + "@babel/core": "^7.24.9", "@babel/preset-env": "^7.24.8", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", - "@storybook/addon-a11y": "^8.2.2", - "@storybook/addon-actions": "^8.2.2", - "@storybook/addon-essentials": "^8.2.2", - "@storybook/addon-interactions": "^8.2.2", - "@storybook/addon-storysource": "^8.2.2", + "@storybook/addon-a11y": "^8.2.5", + "@storybook/addon-actions": "^8.2.5", + "@storybook/addon-essentials": "^8.2.5", + "@storybook/addon-interactions": "^8.2.5", + "@storybook/addon-storysource": "^8.2.5", "@storybook/addon-styling": "^1.3.7", "@storybook/addon-styling-webpack": "^1.0.0", - "@storybook/addon-viewport": "^8.2.2", + "@storybook/addon-viewport": "^8.2.5", "@storybook/addon-webpack5-compiler-babel": "^3.0.3", - "@storybook/blocks": "^8.2.2", - "@storybook/cli": "^8.2.2", - "@storybook/manager-api": "^8.2.2", - "@storybook/react": "^8.2.2", - "@storybook/react-webpack5": "^8.2.2", - "@storybook/source-loader": "^8.2.2", - "@storybook/test": "^8.2.2", - "@storybook/test-runner": "^0.19.0", - "@storybook/theming": "^8.2.2", - "@swc/core": "^1.6.13", + "@storybook/blocks": "^8.2.5", + "@storybook/cli": "^8.2.5", + "@storybook/manager-api": "^8.2.5", + "@storybook/react": "^8.2.5", + "@storybook/react-webpack5": "^8.2.5", + "@storybook/source-loader": "^8.2.5", + "@storybook/test": "^8.2.5", + "@storybook/test-runner": "^0.19.1", + "@storybook/theming": "^8.2.5", + "@swc/core": "^1.7.0", "@swc/jest": "^0.2.36", "@testing-library/react": "^16.0.0", "@types/jest": "^29.5.12", "@types/lodash.merge": "^4.6.9", "@types/lodash.range": "^3.2.9", - "@types/node": "^20.14.10", + "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-test-renderer": "^17.0.9", @@ -59,7 +59,7 @@ "babel-loader": "^9.1.3", "babel-plugin-polyfill-corejs2": "^0.4.11", "babel-plugin-polyfill-regenerator": "^0.6.2", - "chromatic": "^11.5.5", + "chromatic": "^11.5.6", "ecma-version-validator-webpack-plugin": "^1.2.1", "fs-extra": "^11.2.0", "glob": "11.0.0", @@ -69,11 +69,11 @@ "jest-styled-components": "^7.2.0", "memory-fs": "^0.5.0", "npm-run-all": "^4.1.5", - "playwright": "^1.45.1", + "playwright": "^1.45.2", "postcss": "^8.4.39", "postcss-styled-syntax": "^0.6.4", "postcss-syntax": "^0.36.2", - "puppeteer": "^22.13.0", + "puppeteer": "^22.13.1", "react": "^18.3.1", "react-docgen-typescript": "^2.2.2", "react-dom": "^18.3.1", @@ -81,7 +81,7 @@ "react-test-renderer": "^18.3.1", "rimraf": "^6.0.1", "standard-version": "^9.3.2", - "storybook": "^8.2.2", + "storybook": "^8.2.5", "storybook-addon-pseudo-states": "^3.1.1", "styled-components": "^5.3.11", "testcafe": "3.6.2", diff --git a/packages/smarthr-ui/src/components/Button/Button.tsx b/packages/smarthr-ui/src/components/Button/Button.tsx index e722d46a5e..7267dce0ca 100644 --- a/packages/smarthr-ui/src/components/Button/Button.tsx +++ b/packages/smarthr-ui/src/components/Button/Button.tsx @@ -80,7 +80,9 @@ export const Button = forwardRef + const loader = ( + + ) const actualPrefix = !loading && prefix const actualSuffix = loading && !square ? loader : suffix const disabledOnLoading = loading || disabled diff --git a/packages/smarthr-ui/src/components/CompactInformationPanel/VRTCompactInformationPanel.stories.tsx b/packages/smarthr-ui/src/components/CompactInformationPanel/VRTCompactInformationPanel.stories.tsx index 64984c79b5..f36182b2d0 100644 --- a/packages/smarthr-ui/src/components/CompactInformationPanel/VRTCompactInformationPanel.stories.tsx +++ b/packages/smarthr-ui/src/components/CompactInformationPanel/VRTCompactInformationPanel.stories.tsx @@ -9,7 +9,7 @@ import { Type } from './CompactInformationPanel.stories' import { CompactInformationPanel } from '.' export default { - title: 'Data Display(データ表示)/CompactInformationPanel', + title: 'Data Display(データ表示)/CompactInformationPanel(非推奨)', component: CompactInformationPanel, parameters: { withTheming: true, diff --git a/packages/smarthr-ui/src/components/Dialog/ModelessDialog.tsx b/packages/smarthr-ui/src/components/Dialog/ModelessDialog.tsx index bd96c89501..55c0132636 100644 --- a/packages/smarthr-ui/src/components/Dialog/ModelessDialog.tsx +++ b/packages/smarthr-ui/src/components/Dialog/ModelessDialog.tsx @@ -400,6 +400,7 @@ export const ModelessDialog: FC = ({ }} position={position} bounds={draggableBounds} + nodeRef={wrapperRef} >
> type ElementProps = Omit, keyof Props> const lineClamp = tv({ - base: 'smarthr-ui-LineClamp', + slots: { + base: 'smarthr-ui-LineClamp shr-relative', + clampedLine: 'shr-w-full', + shadowElementWrapper: + 'shr-absolute shr-overflow-hidden shr-w-full shr-h-full shr-opacity-0 shr-invisible shr-left-0 shr-top-0 shr-whitespace-normal', + shadowElement: 'shr-absolute shr-w-full shr-top-0 shr-left-0', + }, variants: { maxLines: { - 1: 'shr-inline-block shr-w-full shr-overflow-hidden shr-overflow-ellipsis shr-whitespace-nowrap shr-align-middle', - 2: 'shr-line-clamp-[2]', - 3: 'shr-line-clamp-[3]', - 4: 'shr-line-clamp-[4]', - 5: 'shr-line-clamp-[5]', - 6: 'shr-line-clamp-[6]', + 1: { + clampedLine: + 'shr-inline-block shr-w-full shr-overflow-hidden shr-overflow-ellipsis shr-whitespace-nowrap shr-align-middle', + }, + 2: { + clampedLine: 'shr-line-clamp-[2]', + }, + 3: { + clampedLine: 'shr-line-clamp-[3]', + }, + 4: { + clampedLine: 'shr-line-clamp-[4]', + }, + 5: { + clampedLine: 'shr-line-clamp-[5]', + }, + 6: { + clampedLine: 'shr-line-clamp-[6]', + }, }, }, + compoundVariants: [ + { + maxLines: [2, 3, 4, 5, 6], + className: { + // baseがdisplay:-webkit-boxでないと高さ取得用の要素が表示部分と同じ大きさで表示されないバグを回避するため + base: '[display:-webkit-box]', + }, + }, + ], }) export const LineClamp: FC = ({ @@ -34,30 +62,55 @@ export const LineClamp: FC = ({ className, ...props }) => { - if (maxLines < 1) { - throw new Error('"maxLines" cannot be less than 0.') - } - const [isTooltipVisible, setTooltipVisible] = useState(false) const ref = useRef(null) + const shadowRef = useRef(null) const isMultiLineOverflow = () => { const el = ref.current - return el ? el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight : false + const shadowEl = shadowRef.current + + // -webkit-line-clamp を使った要素ではel.scrollHeightとel.clientHeightの比較だと + // フォントの高さの計算が期待と異なり適切な高さが取得できないためshadowElと比較している + // 参考: https://github.com/kufu/smarthr-ui/pull/4710 + return el && shadowEl + ? shadowEl.clientWidth > el.clientWidth || shadowEl.clientHeight > el.clientHeight + : false } useEffect(() => { setTooltipVisible(isMultiLineOverflow()) }, [maxLines, children]) + if (maxLines < 1) { + throw new Error('"maxLines" cannot be less than 0.') + } + + const { baseStyle, clampedLineStyle, shadowElementWrapperStyle, shadowElementStyle } = + useMemo(() => { + const { base, clampedLine, shadowElementWrapper, shadowElement } = lineClamp({ maxLines }) + return { + baseStyle: base({ className }), + clampedLineStyle: clampedLine(), + shadowElementWrapperStyle: shadowElementWrapper(), + shadowElementStyle: shadowElement(), + } + }, [maxLines, className]) + const ActualLineClamp = () => ( - - {children} + + + {children} + + {/* 切り取られていないテキストの高さを取得するための要素 */} + + + {children} + + ) - const styles = useMemo(() => lineClamp({ maxLines, className }), [className, maxLines]) - return isTooltipVisible ? ( diff --git a/packages/smarthr-ui/src/components/LineClamp/VRTLineClamp.stories.tsx b/packages/smarthr-ui/src/components/LineClamp/VRTLineClamp.stories.tsx index 373f65bdf9..54a6939263 100644 --- a/packages/smarthr-ui/src/components/LineClamp/VRTLineClamp.stories.tsx +++ b/packages/smarthr-ui/src/components/LineClamp/VRTLineClamp.stories.tsx @@ -3,6 +3,7 @@ import { userEvent, within } from '@storybook/test' import React from 'react' import styled from 'styled-components' +import { Button } from '../Button' import { InformationPanel } from '../InformationPanel' import { LineClamp } from './LineClamp' @@ -106,6 +107,47 @@ VRTForcedColors.parameters = { chromatic: { forcedColors: 'active' }, } +export const VRTLineClampInButton: StoryFn = () => ( + <> + + フォントのタイプサイズがline-heightより大きいときにツールチップが表示されないことを確認します + + + +
LineClampが1行でされるボタン
+
+ +
+
LineClampが2行でされるボタン
+
+ +
+
LineClampされないボタン
+
+ +
+
+
+ +) +VRTLineClampInButton.play = async ({ canvasElement }) => { + await new Promise((resolve) => setTimeout(resolve, 500)) // スナップショット時にツールチップを確実に表示させるため + const tooltips = canvasElement.querySelectorAll('.smarthr-ui-Tooltip') + tooltips.forEach((tooltip) => userEvent.hover(tooltip)) +} + const VRTInformationPanel = styled(InformationPanel)` margin-bottom: 24px; ` diff --git a/packages/smarthr-ui/src/components/Loader/Loader.stories.tsx b/packages/smarthr-ui/src/components/Loader/Loader.stories.tsx index e117ea247b..47614e81c1 100644 --- a/packages/smarthr-ui/src/components/Loader/Loader.stories.tsx +++ b/packages/smarthr-ui/src/components/Loader/Loader.stories.tsx @@ -1,5 +1,5 @@ import { StoryFn } from '@storybook/react' -import * as React from 'react' +import React from 'react' import styled, { css } from 'styled-components' import { Loader } from './Loader' @@ -9,45 +9,58 @@ export default { component: Loader, } -export const All: StoryFn = () => ( - <> - - Primary - -
Default
-
- -
-
Small
-
- -
-
With text
-
- -
-
-
+export const All: StoryFn = () => { + // NOTE: 本来は表示を遅延させているが、VRT 向けにデフォルトでは表示を遅延させない。 + const [deferDisplay, setDeferDisplay] = React.useState(false) + return ( + <> + + + Primary + +
Default
+
+ +
+
Small
+
+ +
+
With text
+
+ +
+
+
- - Light - -
Default
-
- -
-
Small
-
- -
-
With text
-
- -
-
-
- -) + + Light + +
Default
+
+ +
+
Small
+
+ +
+
With text
+
+ +
+
+
+ + ) +} All.storyName = 'all' All.parameters = { withTheming: true } diff --git a/packages/smarthr-ui/src/components/Loader/Loader.tsx b/packages/smarthr-ui/src/components/Loader/Loader.tsx index bb46bddf3d..75072fdd32 100644 --- a/packages/smarthr-ui/src/components/Loader/Loader.tsx +++ b/packages/smarthr-ui/src/components/Loader/Loader.tsx @@ -1,4 +1,4 @@ -import React, { FC, HTMLAttributes, ReactNode, useMemo } from 'react' +import React, { ComponentProps, FC, ReactNode, useMemo } from 'react' import { tv } from 'tailwind-variants' import { VisuallyHiddenText } from '../VisuallyHiddenText' @@ -12,13 +12,15 @@ type Props = { text?: ReactNode /** コンポーネントの色調 */ type?: 'primary' | 'light' + /** 表示を遅延させるかどうか */ + deferDisplay?: boolean as?: string | React.ComponentType } -type ElementProps = Omit, keyof Props> +type ElementProps = Omit, keyof Props> const loaderStyle = tv({ slots: { - wrapper: ['smarthr-ui-Loader', 'shr-inline-block', 'shr-overflow-hidden'], + wrapper: ['smarthr-ui-Loader', 'shr-inline-block shr-overflow-hidden'], spinner: [ 'smarthr-ui-Loader-spinner', // Button コンポーネントで使用 'shr-relative', @@ -118,6 +120,13 @@ const loaderStyle = tv({ ], }, }, + deferDisplay: { + true: { + // NOTE: Loaderの表示時間が短い場合のUIのちらつきを抑えるため、opacityの変化でアニメーションの表示を遅延させる + wrapper: 'shr-opacity-0 shr-animate-[loader-fade-in_0s_ease_200ms_forwards]', + }, + false: {}, + }, }, }) export const Loader: FC = ({ @@ -126,12 +135,14 @@ export const Loader: FC = ({ text, type = 'primary', role = 'status', + deferDisplay = true, className, ...props }) => { const { wrapper, spinner, line, cog, cogInner, textSlot } = loaderStyle({ type, size, + deferDisplay, }) const wrapperStyle = useMemo(() => wrapper({ className }), [wrapper, className]) const spinnerStyle = useMemo(() => spinner(), [spinner]) diff --git a/packages/smarthr-ui/src/components/Table/Th.tsx b/packages/smarthr-ui/src/components/Table/Th.tsx index 7fa607d39d..5f768cd022 100644 --- a/packages/smarthr-ui/src/components/Table/Th.tsx +++ b/packages/smarthr-ui/src/components/Table/Th.tsx @@ -135,7 +135,7 @@ export const Th: FC = ({ const sortButton = tv({ base: [ - '-shr-mx-1 -shr-my-0.75 shr-inline-flex shr-w-full shr-gap-x-0.5 shr-px-1 shr-py-0.75 shr-font-bold', + '-shr-mx-1 -shr-my-0.75 shr-inline-flex shr-w-full shr-gap-x-0.5 shr-px-1 shr-py-0.75 shr-font-bold shr-cursor-pointer', // UnstyledButton に stretch がなぜか指定されてて負けてしまうため(UnstyledButton を見直した方がよさそう) '[&]:shr-items-center', ], @@ -149,10 +149,9 @@ const sortButton = tv({ const SortButton: FC & Pick> = ({ align, - className, ...props }) => { - const sortButtonStyle = useMemo(() => sortButton({ align, className }), [align, className]) + const sortButtonStyle = useMemo(() => sortButton({ align }), [align]) return } diff --git a/packages/smarthr-ui/src/components/Tooltip/Tooltip.tsx b/packages/smarthr-ui/src/components/Tooltip/Tooltip.tsx index b2c13ab3ef..21edee254c 100644 --- a/packages/smarthr-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/smarthr-ui/src/components/Tooltip/Tooltip.tsx @@ -4,16 +4,20 @@ import React, { PropsWithChildren, ReactElement, ReactNode, + useCallback, + useMemo, useRef, useState, useSyncExternalStore, } from 'react' import { createPortal } from 'react-dom' +import innerText from 'react-innertext' import { tv } from 'tailwind-variants' import { useEnhancedEffect } from '../../hooks/useEnhancedEffect' import { useId } from '../../hooks/useId' import { Props as BalloonProps } from '../Balloon' +import { VisuallyHiddenText } from '../VisuallyHiddenText' import { TooltipPortal } from './TooltipPortal' @@ -83,7 +87,7 @@ export const Tooltip: FC = ({ const [isVisible, setIsVisible] = useState(false) const [rect, setRect] = useState(null) const ref = useRef(null) - const tooltipId = useId() + const messageId = useId() const fullscreenElement = useSyncExternalStore( subscribeFullscreenChange, getFullscreenElement, @@ -94,51 +98,60 @@ export const Tooltip: FC = ({ setPortalRoot(fullscreenElement ?? document.body) }, [fullscreenElement]) - const getHandlerToShow = + const getHandlerToShow = useCallback( (handler?: (e: T) => void) => - (e: T) => { - handler && handler(e) - if (!ref.current) { - return - } - - if (ellipsisOnly) { - const outerWidth = parseInt( - window - .getComputedStyle(ref.current.parentNode! as HTMLElement, null) - .width.match(/\d+/)![0], - 10, - ) - const wrapperWidth = ref.current.clientWidth - const existsEllipsis = outerWidth >= 0 && outerWidth <= wrapperWidth - if (!existsEllipsis) { + (e: T) => { + handler && handler(e) + if (!ref.current) { return } - } - setRect(ref.current.getBoundingClientRect()) - setIsVisible(true) - } + if (ellipsisOnly) { + const outerWidth = parseInt( + window + .getComputedStyle(ref.current.parentNode! as HTMLElement, null) + .width.match(/\d+/)![0], + 10, + ) + const wrapperWidth = ref.current.clientWidth + const existsEllipsis = outerWidth >= 0 && outerWidth <= wrapperWidth + if (!existsEllipsis) { + return + } + } + + setRect(ref.current.getBoundingClientRect()) + setIsVisible(true) + }, + [ref.current, ellipsisOnly], + ) - const getHandlerToHide = + const getHandlerToHide = useCallback( (handler?: (e: T) => void) => - (e: T) => { - handler && handler(e) - setIsVisible(false) - } + (e: T) => { + handler && handler(e) + setIsVisible(false) + }, + [setIsVisible], + ) + const hiddenText = useMemo(() => innerText(message), [message]) const isIcon = triggerType === 'icon' const styles = tooltip({ isIcon, className }) - const childrenWithProps = - ariaDescribedbyTarget === 'inner' - ? React.cloneElement(children as ReactElement, { 'aria-describedby': tooltipId }) - : children + const isInnerTarget = ariaDescribedbyTarget === 'inner' + const childrenWithProps = useMemo( + () => + isInnerTarget + ? React.cloneElement(children as ReactElement, { 'aria-describedby': messageId }) + : children, + [children, isInnerTarget, messageId], + ) return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions,smarthr/a11y-delegate-element-has-role-presentation = ({ createPortal( = ({ portalRoot, )} {childrenWithProps} + {hiddenText} ) } diff --git a/packages/smarthr-ui/src/components/Tooltip/TooltipPortal.tsx b/packages/smarthr-ui/src/components/Tooltip/TooltipPortal.tsx index be415c7011..e59df663a4 100644 --- a/packages/smarthr-ui/src/components/Tooltip/TooltipPortal.tsx +++ b/packages/smarthr-ui/src/components/Tooltip/TooltipPortal.tsx @@ -7,7 +7,6 @@ import { getTooltipRect } from './tooltipHelper' type Props = { message: ReactNode - id: string isVisible: boolean parentRect: DOMRect | null isIcon?: boolean @@ -34,7 +33,6 @@ const tooltipPortal = tv({ export const TooltipPortal: FC = ({ message, - id, isVisible, parentRect, isIcon = false, @@ -143,7 +141,7 @@ export const TooltipPortal: FC = ({ }, [isMultiLine, parentRect, rect.$height, rect.$width, rect.left, rect.top]) return ( -