diff --git a/code/addons/a11y/src/components/VisionSimulator.tsx b/code/addons/a11y/src/components/VisionSimulator.tsx index 4783a556ed57..ec8d1050a792 100644 --- a/code/addons/a11y/src/components/VisionSimulator.tsx +++ b/code/addons/a11y/src/components/VisionSimulator.tsx @@ -21,10 +21,11 @@ const Hidden = styled.div({ const ColorIcon = styled.span<{ $filter: string }>( { background: 'linear-gradient(to right, #F44336, #FF9800, #FFEB3B, #8BC34A, #2196F3, #9C27B0)', - borderRadius: '1rem', + borderRadius: 14, display: 'block', - height: '1rem', - width: '1rem', + flexShrink: 0, + height: 14, + width: 14, }, ({ $filter }) => ({ filter: filters[$filter as keyof typeof filters].filter || 'none', diff --git a/code/core/src/components/components/ActionList/ActionList.stories.tsx b/code/core/src/components/components/ActionList/ActionList.stories.tsx index d2f011af0b59..848ecc02861e 100644 --- a/code/core/src/components/components/ActionList/ActionList.stories.tsx +++ b/code/core/src/components/components/ActionList/ActionList.stories.tsx @@ -62,7 +62,9 @@ export const Default = meta.story({ - + + + Active with an icon @@ -77,10 +79,59 @@ export const Default = meta.story({ Some very long text which will ellipsize when the container is too narrow + + + + + + +

Title

+ Description +
+
+
+ + + + + + +

Some very long text which is going to wrap around

+ Here is a very long description which is also going to wrap around +
+
+
), }); +export const Listbox = meta.story({ + render: () => ( + <> + + + + + + Option + + + + + + Selected + + + + + + Visually disabled + + + + ), +}); + export const Groups = meta.story({ render: () => ( <> diff --git a/code/core/src/components/components/ActionList/ActionList.tsx b/code/core/src/components/components/ActionList/ActionList.tsx index 73c75393b077..0c6abee84258 100644 --- a/code/core/src/components/components/ActionList/ActionList.tsx +++ b/code/core/src/components/components/ActionList/ActionList.tsx @@ -1,5 +1,6 @@ import React, { type ComponentProps, forwardRef } from 'react'; +import { darken, transparentize } from 'polished'; import type { TransitionStatus } from 'react-transition-state'; import { styled } from 'storybook/theming'; @@ -16,22 +17,53 @@ const ActionListItem = styled.li<{ justifyContent: 'space-between', flex: '0 0 auto', overflow: 'hidden', + minHeight: 32, gap: 4, fontSize: theme.typography.size.s1, fontWeight: active ? theme.typography.weight.bold : theme.typography.weight.regular, - color: active ? theme.color.secondary : theme.color.defaultText, - '--listbox-item-muted-color': active ? theme.color.secondary : theme.color.mediumdark, + color: active ? 'var(--listbox-item-active-color)' : theme.color.defaultText, + '--listbox-item-active-color': + theme.base === 'light' ? darken(0.1, theme.color.secondary) : theme.color.secondary, + '--listbox-item-muted-color': active + ? 'var(--listbox-item-active-color)' + : theme.color.mediumdark, + + '&[aria-disabled="true"]': { + opacity: 0.5, + cursor: 'not-allowed', + }, + '&[aria-selected="true"]': { + color: 'var(--listbox-item-active-color)', + fontWeight: theme.typography.weight.bold, + '--listbox-item-muted-color': 'var(--listbox-item-active-color)', + }, '&:not(:hover, :has(:focus-visible)) svg + input': { position: 'absolute', opacity: 0, }, + '&[role="option"]': { + cursor: 'pointer', + borderRadius: theme.input.borderRadius, + outlineOffset: -2, + padding: '0 9px', + gap: 8, + + '&:hover': { + background: transparentize(0.86, theme.color.secondary), + }, + '&:focus-visible': { + outline: `2px solid ${theme.color.secondary}`, + }, + }, + '@supports (interpolate-size: allow-keywords)': { interpolateSize: 'allow-keywords', - transition: 'all var(--transition-duration, 0.2s)', transitionBehavior: 'allow-discrete', + transitionDuration: 'var(--transition-duration, 0.2s)', + transitionProperty: 'opacity, block-size, content-visibility', }, '@media (prefers-reduced-motion: reduce)': { @@ -83,12 +115,14 @@ const ActionListHoverItem = styled(ActionListItem)<{ targetId: string }>(({ targ }, })); -const StyledButton = styled(Button)({ +const StyledButton = styled(Button)(({ size }) => ({ + gap: size === 'small' ? 6 : 8, + '&:focus-visible': { // Prevent focus outline from being cut off by overflow: hidden outlineOffset: -2, }, -}); +})); const StyledToggleButton = styled(ToggleButton)({ '&:focus-visible': { @@ -116,6 +150,8 @@ const ActionListToggle = forwardRef ({ + height: 'auto', + minHeight: 32, flex: '0 1 100%', textAlign: 'start', justifyContent: 'space-between', @@ -137,33 +173,41 @@ const ActionListLink = ( props: ComponentProps & React.AnchorHTMLAttributes ) => ; -const ActionListText = styled.div({ +const ActionListText = styled.div(({ theme }) => ({ display: 'flex', - alignItems: 'center', - gap: 8, + flexDirection: 'column', + justifyContent: 'center', flexGrow: 1, minWidth: 0, padding: '8px 0', lineHeight: '16px', - '& span': { + '& > *': { + margin: 0, + whiteSpace: 'normal', + }, + '& > span': { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, + '& small': { + fontSize: 'inherit', + color: theme.textMutedColor, + }, '&:first-child': { paddingLeft: 8, }, '&:last-child': { paddingRight: 8, }, - 'button > &:first-child': { + 'button > &:first-child, [role="option"] > &:first-child': { paddingLeft: 0, }, - 'button > &:last-child': { + 'button > &:last-child, [role="option"] > &:last-child': { paddingRight: 0, }, -}); +})); const ActionListIcon = styled.div({ display: 'flex', diff --git a/code/core/src/components/components/Select/Select.tsx b/code/core/src/components/components/Select/Select.tsx index 4ac310957c5f..b9287463839a 100644 --- a/code/core/src/components/components/Select/Select.tsx +++ b/code/core/src/components/components/Select/Select.tsx @@ -317,9 +317,9 @@ export const Select = forwardRef( // wrap setActiveOption to handle selection. We never close the listbox // in that scenario. const setActiveOption = useCallback( - (option: Option | ResetOption) => { + (option: Option | ResetOption, changeSelection = true) => { setActiveOptionState(optionOrResetToInternal(option)); - if (!multiSelect) { + if (!multiSelect && changeSelection) { handleSelectOption(optionOrResetToInternal(option)); } }, @@ -559,6 +559,7 @@ export const Select = forwardRef( key={option.value === undefined ? 'sb-reset' : String(option.value)} title={option.title} description={option.description} + aside={option.aside} icon={ !isReset && multiSelect ? ( // Purely decorative. @@ -576,7 +577,7 @@ export const Select = forwardRef( handleClose(); } }} - onFocus={() => setActiveOption(externalOption)} + onFocus={() => setActiveOption(externalOption, false)} shouldLookDisabled={isReset && selectedOptions.length === 0 && multiSelect} onKeyDown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { diff --git a/code/core/src/components/components/Select/SelectOption.tsx b/code/core/src/components/components/Select/SelectOption.tsx index 178a37313130..be9447889f26 100644 --- a/code/core/src/components/components/Select/SelectOption.tsx +++ b/code/core/src/components/components/Select/SelectOption.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { darken, transparentize } from 'polished'; -import { styled } from 'storybook/theming'; +import { ActionList } from '../..'; export interface SelectOptionProps { /** @@ -17,9 +16,12 @@ export interface SelectOptionProps { /** Secondary text or description not necessary to identify the option. */ description?: string; - /** Decorative icon. */ + /** Decorative icon, displayed to the left of the title and description. */ icon?: React.ReactNode; + /** Extra content, displayed to the right of the title and description. */ + aside?: React.ReactNode; + /** Optional rendering of the option. Use sparingly. */ children?: React.ReactNode; @@ -40,67 +42,12 @@ export interface SelectOptionProps { shouldLookDisabled: boolean; } -const Item = styled('li')(({ theme }) => ({ - padding: '6px 12px', - fontSize: 12, - lineHeight: 1.5, - background: 'transparent', - color: theme.color.defaultText, - cursor: 'pointer', - borderRadius: 4, - '&[aria-disabled="true"]': { - opacity: 0.5, - cursor: 'not-allowed', - }, - '&[aria-selected="true"]': { - color: theme.base === 'light' ? darken(0.1, theme.color.secondary) : theme.color.secondary, - fontWeight: theme.typography.weight.bold, - }, - ':hover': { - background: transparentize(0.93, theme.color.secondary), - }, - ':focus-visible': { - background: theme.background.hoverable, - outline: `2px solid ${theme.barSelectedColor}`, - outlineOffset: 1, - borderRadius: 2, - }, - display: 'flex', - alignItems: 'flex-start', - gap: 8, -})); - -const Row = styled('div')({ - display: 'flex', - flexDirection: 'row', - gap: 4, - alignItems: 'center', -}); - -const Col = styled('div')({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, -}); - -const Icon = styled('span')(() => ({ - display: 'block', - height: '1rem', - width: '1rem', -})); - -const Title = styled('span')(({}) => ({})); - -const Description = styled('span')(({ theme }) => ({ - fontSize: 11, - color: theme.textMutedColor, -})); - export const SelectOption: React.FC = ({ id, title, description, icon, + aside, children, isSelected, isActive, @@ -111,7 +58,7 @@ export const SelectOption: React.FC = ({ ...props }) => { return ( - = ({ onKeyDown={onKeyDown} > {children ?? ( - - {icon && {icon}} - - {title} - {description && {description}} - - + <> + {icon && {icon}} + +

{title}

+ {description && {description}} +
+ {aside} + )} -
+ ); }; diff --git a/code/core/src/components/components/Select/helpers.tsx b/code/core/src/components/components/Select/helpers.tsx index b115812ffe45..56500dd66a29 100644 --- a/code/core/src/components/components/Select/helpers.tsx +++ b/code/core/src/components/components/Select/helpers.tsx @@ -14,6 +14,7 @@ export interface Option { title: string; description?: string; icon?: React.ReactNode; + aside?: React.ReactNode; value: Value; } export interface InternalOption extends Omit { diff --git a/code/core/src/manager/components/layout/useDragging.ts b/code/core/src/manager/components/layout/useDragging.ts index 88727a1ff2b3..1028f45df9f5 100644 --- a/code/core/src/manager/components/layout/useDragging.ts +++ b/code/core/src/manager/components/layout/useDragging.ts @@ -34,7 +34,7 @@ export function useDragging({ useEffect(() => { const panelResizer = panelResizerRef.current; const sidebarResizer = sidebarResizerRef.current; - const previewIframe = document.querySelector('#storybook-preview-wrapper') as HTMLIFrameElement; + const previewIframe = document.querySelector('#storybook-preview-iframe') as HTMLIFrameElement; let draggedElement: typeof panelResizer | typeof sidebarResizer | null = null; const onDragStart = (e: MouseEvent) => { diff --git a/code/core/src/manager/components/preview/FramesRenderer.tsx b/code/core/src/manager/components/preview/FramesRenderer.tsx index 03a47e247676..5aa1547e2508 100644 --- a/code/core/src/manager/components/preview/FramesRenderer.tsx +++ b/code/core/src/manager/components/preview/FramesRenderer.tsx @@ -8,7 +8,7 @@ import { Consumer } from 'storybook/manager-api'; import { Global, styled } from 'storybook/theming'; import type { CSSObject } from 'storybook/theming'; -import { IFrame } from './Iframe'; +import { Viewport } from './Viewport'; import type { FramesRendererProps } from './utils/types'; const getActive = (refId: FramesRendererProps['refId'], refs: FramesRendererProps['refs']) => { @@ -104,21 +104,9 @@ export const FramesRenderer: FC = ({ ); }} - {Object.entries(frames).map(([id, src]) => { - return ( - -