From 109be012440be165c15ac74a7ee1e833fac1bac8 Mon Sep 17 00:00:00 2001 From: yasuda_shin Date: Thu, 26 Sep 2024 14:16:32 +0900 Subject: [PATCH] feat(spindle-ui): add toggle tip component --- .../tokens/color/theme-light.json | 4 +- .../spindle-ui/src/Toggletip/Toggletip.css | 147 +++++++++++ .../Toggletip/Toggletip.stories.example.tsx | 244 ++++++++++++++++++ .../src/Toggletip/Toggletip.stories.mdx | 94 +++++++ .../spindle-ui/src/Toggletip/Toggletip.tsx | 125 +++++++++ packages/spindle-ui/src/Toggletip/index.ts | 1 + packages/spindle-ui/src/Toggletip/union.png | Bin 0 -> 286 bytes packages/spindle-ui/src/index.css | 1 + packages/spindle-ui/src/index.ts | 1 + 9 files changed, 615 insertions(+), 2 deletions(-) create mode 100644 packages/spindle-ui/src/Toggletip/Toggletip.css create mode 100644 packages/spindle-ui/src/Toggletip/Toggletip.stories.example.tsx create mode 100644 packages/spindle-ui/src/Toggletip/Toggletip.stories.mdx create mode 100644 packages/spindle-ui/src/Toggletip/Toggletip.tsx create mode 100644 packages/spindle-ui/src/Toggletip/index.ts create mode 100644 packages/spindle-ui/src/Toggletip/union.png diff --git a/packages/spindle-tokens/tokens/color/theme-light.json b/packages/spindle-tokens/tokens/color/theme-light.json index a37ea2b5c..44d8d122a 100644 --- a/packages/spindle-tokens/tokens/color/theme-light.json +++ b/packages/spindle-tokens/tokens/color/theme-light.json @@ -355,7 +355,7 @@ "attributes": { "category": "color" }, - "comment": "Do : snackbar / tooltip / toast , , Amebaのメインカラー白に対してコントラストを担保しつつ、暗い印象を持たず、, ポジティブでもネガティブでもない中立の意味合いを表現できつつ、文字に対してコントラストが担保できる色", + "comment": "Do : snackbar / toggletip / toast , , Amebaのメインカラー白に対してコントラストを担保しつつ、暗い印象を持たず、, ポジティブでもネガティブでもない中立の意味合いを表現できつつ、文字に対してコントラストが担保できる色", "value": "{Color.Primitive.Gray.80.value}" }, "Accent Neutral Medium Emphasis": { @@ -527,4 +527,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/spindle-ui/src/Toggletip/Toggletip.css b/packages/spindle-ui/src/Toggletip/Toggletip.css new file mode 100644 index 000000000..0c6a54473 --- /dev/null +++ b/packages/spindle-ui/src/Toggletip/Toggletip.css @@ -0,0 +1,147 @@ +/* + * Toggletip + * NOTE: Styles can be overridden with "--Toggletip-*" variables +*/ +:root { + --Toggletip-onFocus-outlineColor: var(--color-focus-clarity); + --Toggletip-menu-gap: 8px; +} + +.spui-Toggletip { + position: relative; + width: fit-content; +} + +.spui-Toggletip-menu { + animation: 0.3s spui-Toggletip-fade-in; + border: none; + display: none; + list-style: none; + opacity: 0; + padding: 8px; + transition: + opacity 300ms, + display 300ms allow-discrete, + overlay 300ms allow-discrete; +} + +.spui-Toggletip-menu:popover-open { + display: block; + opacity: 1; + + @starting-style { + opacity: 0; + } +} + +.spui-Toggletip-inner { + background: var(--color-surface-accent-neutral-high-emphasis); + border-radius: 8px; + box-sizing: border-box; + margin: 0; + padding: 12px; + position: relative; + width: 206px; +} + +.spui-Toggletip-inner::after { + background: url('union.png') left top no-repeat; + background-size: 16px 7px; + content: ''; + display: block; + height: 7px; + position: absolute; + width: 16px; +} + +.spui-Toggletip-menu--topRight { + inset-area: top right; + margin: 0 0 var(--Toggletip-menu-gap) -44px; +} + +.spui-Toggletip-inner--topRight::after { + bottom: -7px; + left: 16px; + transform: rotate(-180deg); +} + +.spui-Toggletip-menu--topCenter { + inset-area: top center; + margin: 0 0 var(--Toggletip-menu-gap); +} + +.spui-Toggletip-inner--topCenter::after { + bottom: -7px; + left: 50%; + margin-left: -8px; + transform: rotate(-180deg); +} + +.spui-Toggletip-menu--topLeft { + inset-area: top left; + margin: 0 -44px var(--Toggletip-menu-gap) 0; +} + +.spui-Toggletip-inner--topLeft::after { + bottom: -7px; + margin-left: -8px; + right: 16px; + transform: rotate(-180deg); +} + +.spui-Toggletip-menu--bottomRight { + inset-area: bottom span-right; + margin: var(--Toggletip-menu-gap) 0 0 -22px; +} + +.spui-Toggletip-inner--bottomRight::after { + left: 16px; + top: -7px; +} + +.spui-Toggletip-menu--bottomCenter { + inset-area: bottom center; + margin: var(--Toggletip-menu-gap) 0 0; +} + +.spui-Toggletip-inner--bottomCenter::after { + left: 50%; + margin-left: -8px; + top: -7px; +} + +.spui-Toggletip-menu--bottomLeft { + inset-area: bottom span-left; + margin: var(--Toggletip-menu-gap) -22px 0 0; +} + +.spui-Toggletip-inner--bottomLeft::after { + margin-left: -8px; + right: 16px; + top: -7px; +} + +.spui-Toggletip-menuButton { + align-items: center; + background-color: var(--color-surface-primary); + border: none; + display: flex; + font-size: 1em; + position: relative; + transition: background-color 0.3s; + width: 100%; +} + +.spui-Toggletip-label { + color: #fff; + font-size: 0.875rem; + font-weight: bold; + line-height: 1.4; + margin: 0; +} + +@media (prefers-reduced-motion: reduce) { + .spui-Toggletip-menu { + transition-duration: 0.1s; + } +} diff --git a/packages/spindle-ui/src/Toggletip/Toggletip.stories.example.tsx b/packages/spindle-ui/src/Toggletip/Toggletip.stories.example.tsx new file mode 100644 index 000000000..c43ee48bf --- /dev/null +++ b/packages/spindle-ui/src/Toggletip/Toggletip.stories.example.tsx @@ -0,0 +1,244 @@ +import React, { useState, useRef } from 'react'; + +import { QuestionmarkCircleFill } from '../Icon'; +import { Toggletip } from './Toggletip'; + +export function TopRight() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} + +export function TopCenter() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} + +export function TopLeft() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} + +export function BottomRight() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} + +export function BottomCenter() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} + +export function BottomLeft() { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const onMouseOver = () => { + setOpen(true); + }; + const onMouseOut = () => { + setOpen(false); + }; + + return ( +
+
+ +
+ +
+ + ここに補足の説明を表示 + +
+
+
+ ); +} diff --git a/packages/spindle-ui/src/Toggletip/Toggletip.stories.mdx b/packages/spindle-ui/src/Toggletip/Toggletip.stories.mdx new file mode 100644 index 000000000..d472e64ca --- /dev/null +++ b/packages/spindle-ui/src/Toggletip/Toggletip.stories.mdx @@ -0,0 +1,94 @@ +import { Description, Meta, Story, Source } from '@storybook/addon-docs/blocks'; +import { Toggletip } from './Toggletip'; +import { + TopRight, + TopCenter, + TopLeft, + BottomRight, + BottomCenter, + BottomLeft, +} from './Toggletip.stories.example'; + +# Toggletip + + + + + + + +`} +/> + +## 指定できるプロパティ +### Toggletip.List + + - `open`(必須): Toggletipの開閉状態です。Toggletipは`open`プロパティの値を見て開閉を行うため、`onClick`や`onClose`内で`open`プロパティを変更してください。 + + + - `triggerRef`(必須): Triggerボタンのrefを指定してください。 + + + - `onClose`(必須): `open`プロパティをfalseにするための処理を指定してください。 + + + - `position`(任意): Toggletipをボタンに対してどの位置に開くかを指定します。デフォルトは`leftTop`で、その他にも`topLeft`, `topCenter`, `topRight`, `rightTop`, `rightCenter`, `rightBottom`, `bottomLeft`, `bottomCenter`, `bottomRight`, `leftTop`, `leftCenter`, `leftBottom`を指定することができます。 + + + - `variant`(任意): Toggletipの種類を指定します。デフォルトは`text`で、その他にも`textWithIcon`, `headWithIcon`, `headWithIconAndCaption`を指定することができます。 + + + - `id`(任意): idを指定することができます。 + + +### Toggletip.ListItem + + - `onClick`(必須): 各要素をクリックした際に実行したい処理を指定してください。 + + + - `icon`(任意): Toggletip内にアイコンを表示したい場合は、指定することができます。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/spindle-ui/src/Toggletip/Toggletip.tsx b/packages/spindle-ui/src/Toggletip/Toggletip.tsx new file mode 100644 index 000000000..239d5ba93 --- /dev/null +++ b/packages/spindle-ui/src/Toggletip/Toggletip.tsx @@ -0,0 +1,125 @@ +/// + +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +declare module 'react' { + interface CSSProperties { + anchorName?: string; + positionAnchor?: string; + } +} + +type Variant = + | 'text' + | 'textWithIcon' + | 'headWithIcon' + | 'headWithIconAndCaption'; + +type Position = + | 'topLeft' + | 'topCenter' + | 'topRight' + | 'bottomLeft' + | 'bottomCenter' + | 'bottomRight'; + +interface DefaultProps { + children: React.ReactNode; +} + +interface BodyProps extends DefaultProps { + id: string; + onClose?: (state: 'open' | 'closed') => void; + open?: boolean; + popover?: 'auto' | 'manual'; + position?: Position; + triggerRef: React.RefObject; + variant?: Variant; +} + +export const BLOCK_NAME = 'spui-Toggletip'; + +const Caption = ({ children }: DefaultProps) => { + return

{children}

; +}; + +const Frame = ({ children }: DefaultProps) => { + return
{children}
; +}; + +const Body = ({ + children, + onClose, + open, + popover = 'auto', + position = 'topCenter', + variant = 'text', + triggerRef, +}: BodyProps) => { + const menuEl = useRef(null); + const [anchorName, setAnchorName] = useState(''); + + // TODO: close with menu item click + // NOTE: esc, backdrop click do not fire close event + const handleClose = useCallback( + (event: React.ToggleEvent) => { + onClose?.(event.newState); + }, + [onClose], + ); + + useEffect(() => { + // triggerRef is for backword compatibility, id string is more reliable + if (triggerRef.current) { + setAnchorName(triggerRef.current.id); + } + }, [triggerRef]); + + useEffect(() => { + // initial state + if (open) { + // @ts-ignore + menuEl.current?.showPopover(); + } else { + // @ts-ignore + menuEl.current?.hidePopover(); + } + }, [menuEl, open]); + + return ( +
+
+ {children} +
+
+ ); +}; + +const Label = ({ children }: DefaultProps) => { + return

{children}

; +}; + +export const Toggletip = { + Caption, + Frame, + Body, + Label, +}; diff --git a/packages/spindle-ui/src/Toggletip/index.ts b/packages/spindle-ui/src/Toggletip/index.ts new file mode 100644 index 000000000..90bd94f32 --- /dev/null +++ b/packages/spindle-ui/src/Toggletip/index.ts @@ -0,0 +1 @@ +export { Toggletip } from './Toggletip'; diff --git a/packages/spindle-ui/src/Toggletip/union.png b/packages/spindle-ui/src/Toggletip/union.png new file mode 100644 index 0000000000000000000000000000000000000000..97cf449d875e0f92bd2fbb0cc86c1ecdb128e152 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^3P8-q!3HG#?#OKdQk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAu1bt(ragvD$|tjxEf)RBXK|nQ-{k_s eC;e|Z>G@L2qNC!sUi}JmK7*&LpUXO@geCxY%Vr<| literal 0 HcmV?d00001 diff --git a/packages/spindle-ui/src/index.css b/packages/spindle-ui/src/index.css index 2bd0f61a4..9eee57652 100644 --- a/packages/spindle-ui/src/index.css +++ b/packages/spindle-ui/src/index.css @@ -21,3 +21,4 @@ @import './InlineNotification/InlineNotification.css'; @import './SegmentedControl/SegmentedControl.css'; @import './NavigationTab/index.css'; +@import './Toggletip/Toggletip.css'; diff --git a/packages/spindle-ui/src/index.ts b/packages/spindle-ui/src/index.ts index 58157f906..9fd97d7c4 100644 --- a/packages/spindle-ui/src/index.ts +++ b/packages/spindle-ui/src/index.ts @@ -14,3 +14,4 @@ export { DropdownMenu } from './DropdownMenu'; export { SnackBar } from './SnackBar'; export { InlineNotification } from './InlineNotification'; export { SegmentedControl } from './SegmentedControl'; +export { Toggletip } from './Toggletip';