From e2763913ee2ac9db97c769d1aecf1bb965199a0c Mon Sep 17 00:00:00 2001 From: Georgii Lobko Date: Wed, 18 Mar 2026 11:14:26 +0100 Subject: [PATCH 1/4] demo: Toggleable tokens --- pages/token-group/toggleable.page.tsx | 45 +++++++++++++++++++++++++++ pages/token/simple.page.tsx | 11 +++++++ src/token/interfaces.ts | 13 +++++++- src/token/internal.tsx | 25 +++++++++++++-- src/token/styles.scss | 10 ++++++ 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 pages/token-group/toggleable.page.tsx diff --git a/pages/token-group/toggleable.page.tsx b/pages/token-group/toggleable.page.tsx new file mode 100644 index 0000000000..7077ba8799 --- /dev/null +++ b/pages/token-group/toggleable.page.tsx @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import SpaceBetween from '~components/space-between'; +import Token, { TokenProps } from '~components/token'; + +const generateItems = (numberOfItems: number) => { + return [...new Array(numberOfItems)].map((_item, index) => ({ + id: index + '', + label: `Item ${index + 1}`, + })) as TokenProps[]; +}; + +export default function TokenGroupToggleablePage() { + const items = generateItems(12); + const [pressedTokens, setPressedTokens] = useState(new Set()); + + return ( + <> +

Toggleable tokens

+ + {items.map((token, index) => ( + { + setPressedTokens(prev => { + const next = new Set(prev); + if (event.detail.pressed) { + next.add(token.id!); + } else { + next.delete(token.id!); + } + return next; + }); + }} + /> + ))} + + + ); +} diff --git a/pages/token/simple.page.tsx b/pages/token/simple.page.tsx index 0ba52ab3e1..8e857d5a2c 100644 --- a/pages/token/simple.page.tsx +++ b/pages/token/simple.page.tsx @@ -24,6 +24,7 @@ const LONG_LABEL = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed export default function GenericTokenPage() { const [files, setFiles] = useState(range(0, 4)); + const [pressed, setPressed] = useState(false); const onDismiss = (itemIndex: number) => { const newItems = [...files]; @@ -34,6 +35,16 @@ export default function GenericTokenPage() { return (

Standalone token

+

Toggle

+ + setPressed(event.detail.pressed)} + /> +

Inline

diff --git a/src/token/interfaces.ts b/src/token/interfaces.ts index 47bfa0d75a..0ecf73e25f 100644 --- a/src/token/interfaces.ts +++ b/src/token/interfaces.ts @@ -69,8 +69,19 @@ export interface TokenProps extends BaseComponentProps { * Only applies to plain text labels. */ tooltipContent?: string; + + pressed?: boolean; + + /** + * Called when the user changes their selection. + * The event `detail` contains the current value for the `pressed` property. + */ + onChange?: NonCancelableEventHandler; } export namespace TokenProps { - export type Variant = 'normal' | 'inline'; + export type Variant = 'normal' | 'inline' | 'inline-toggle'; + export interface ChangeDetail { + pressed: boolean; + } } diff --git a/src/token/internal.tsx b/src/token/internal.tsx index bd16485a73..414e8be0fa 100644 --- a/src/token/internal.tsx +++ b/src/token/internal.tsx @@ -8,6 +8,7 @@ import { useResizeObserver, useUniqueId, warnOnce } from '@cloudscape-design/com import { getBaseProps } from '../internal/base-component'; import Option from '../internal/components/option'; +import { fireNonCancelableEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import LiveRegion from '../live-region/internal'; import Tooltip from '../tooltip/internal.js'; @@ -39,6 +40,8 @@ function InternalToken({ dismissLabel, onDismiss, tooltipContent, + pressed, + onChange, // Internal role, @@ -53,7 +56,7 @@ function InternalToken({ const labelRef = useRef(null); const [showTooltip, setShowTooltip] = useState(false); const [isEllipsisActive, setIsEllipsisActive] = useState(false); - const isInline = variant === 'inline'; + const isInline = variant === 'inline' || variant === 'inline-toggle'; const ariaLabelledbyId = useUniqueId(); const isLabelOverflowing = () => { @@ -100,6 +103,13 @@ function InternalToken({ // Use span for inline tokens (e.g. inside contentEditable) to avoid block-level elements breaking text flow. const SpanOrDivTag = isInline ? 'span' : 'div'; + const toggleProps = + variant === 'inline-toggle' + ? { + 'aria-pressed': pressed, + } + : {}; + return ( { + if (variant !== 'inline-toggle') { + return; + } + event.preventDefault(); + + fireNonCancelableEvent(onChange, { pressed: !pressed }); + }} + {...toggleProps} >