Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9949fc9
Tweak ActionList.Button spacing
ghengeveld Dec 5, 2025
086696c
Redesign and refactor viewports tool
ghengeveld Dec 5, 2025
4e4f082
Left-align dimension controls
ghengeveld Dec 5, 2025
ef735a4
Refactor viewport component and update SizeInput parsing logic
ghengeveld Dec 8, 2025
fc46be4
Add corner drag handle
ghengeveld Dec 8, 2025
4303605
Enhance SizeInput and Viewport components
ghengeveld Dec 9, 2025
879b3c2
Merge branch 'next' into viewport-viewer
ghengeveld Dec 10, 2025
bcbec81
Merge branch 'next' into viewport-viewer
ghengeveld Jan 7, 2026
bb31347
Restore Viewport toolbar menu, update Select to use ActionList compon…
ghengeveld Jan 7, 2026
4352437
Remove viewport selector on canvas and add dimensions to viewport sel…
ghengeveld Jan 7, 2026
a66780b
Update E2E selectors
ghengeveld Jan 7, 2026
f2bd41f
Avoid referencing localhost
ghengeveld Jan 7, 2026
920de71
Update E2E selectors
ghengeveld Jan 7, 2026
1a58d9c
Fix isRotated
ghengeveld Jan 7, 2026
602ddd7
Fix strikethrough on ChecklistWidget
ghengeveld Jan 7, 2026
94c22bf
Restore aria label
ghengeveld Jan 7, 2026
ca6a9be
Fix pseudo states
ghengeveld Jan 7, 2026
4a58479
Fix invalid role=listbox nesting
ghengeveld Jan 7, 2026
e83efbf
Remove unused imports
ghengeveld Jan 7, 2026
1c34a8d
Fix text alignment
ghengeveld Jan 8, 2026
8f6d7ea
Fix invalid nesting
ghengeveld Jan 8, 2026
72b4543
Fix pseudo states and custom content rendering
ghengeveld Jan 8, 2026
57cd271
Remove unused prop
ghengeveld Jan 8, 2026
c952397
Show viewport size on draghandle
ghengeveld Dec 16, 2025
1d7c023
Fix sidebar and panel drag resizing
ghengeveld Jan 8, 2026
9508e97
Merge branch 'next' into viewport-viewer
ghengeveld Jan 8, 2026
e84ab86
Fix vision simulator icons
ghengeveld Jan 8, 2026
c977c7e
Don't apply viewport in docs mode
ghengeveld Jan 9, 2026
5cf14fe
Fix input labels
ghengeveld Jan 9, 2026
c115fef
Fix coercing of isRotated value to initial value, and fix Viewport st…
ghengeveld Jan 9, 2026
04ed550
Rename 'right' prop to 'aside'
ghengeveld Jan 9, 2026
ccbcc9a
Don't change selection through onFocus handler for SelectOption, beca…
ghengeveld Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions code/addons/a11y/src/components/VisionSimulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export const Default = meta.story({
</ActionList.Item>
<ActionList.Item active>
<ActionList.Action>
<CheckIcon />
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>Active with an icon</ActionList.Text>
<Shortcut keys={['⌘', 'A']} />
</ActionList.Action>
Expand All @@ -77,10 +79,59 @@ export const Default = meta.story({
<span>Some very long text which will ellipsize when the container is too narrow</span>
</ActionList.Text>
</ActionList.Item>
<ActionList.Item>
<ActionList.Action>
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>
<p>Title</p>
<small>Description</small>
</ActionList.Text>
</ActionList.Action>
</ActionList.Item>
<ActionList.Item active>
<ActionList.Action>
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>
<p>Some very long text which is going to wrap around</p>
<small>Here is a very long description which is also going to wrap around</small>
</ActionList.Text>
</ActionList.Action>
</ActionList.Item>
</ActionList>
),
});

export const Listbox = meta.story({
render: () => (
<>
<ActionList role="listbox" aria-label="Sample listbox">
<ActionList.Item role="option">
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>Option</ActionList.Text>
</ActionList.Item>
<ActionList.Item role="option" aria-selected="true">
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>Selected</ActionList.Text>
</ActionList.Item>
<ActionList.Item role="option" aria-disabled="true">
<ActionList.Icon>
<CheckIcon />
</ActionList.Icon>
<ActionList.Text>Visually disabled</ActionList.Text>
</ActionList.Item>
</ActionList>
</>
),
});

export const Groups = meta.story({
render: () => (
<>
Expand Down
68 changes: 56 additions & 12 deletions code/core/src/components/components/ActionList/ActionList.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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)': {
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -116,6 +150,8 @@ const ActionListToggle = forwardRef<HTMLButtonElement, ComponentProps<typeof Sty
);

const ActionListAction = styled(ActionListButton)(({ theme }) => ({
height: 'auto',
minHeight: 32,
flex: '0 1 100%',
textAlign: 'start',
justifyContent: 'space-between',
Expand All @@ -137,33 +173,41 @@ const ActionListLink = (
props: ComponentProps<typeof ActionListAction> & React.AnchorHTMLAttributes<HTMLAnchorElement>
) => <ActionListAction as="a" {...props} />;

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',
Expand Down
7 changes: 4 additions & 3 deletions code/core/src/components/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,9 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
// 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));
}
},
Expand Down Expand Up @@ -559,6 +559,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
key={option.value === undefined ? 'sb-reset' : String(option.value)}
title={option.title}
description={option.description}
aside={option.aside}
icon={
!isReset && multiSelect ? (
// Purely decorative.
Expand All @@ -576,7 +577,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
handleClose();
}
}}
onFocus={() => setActiveOption(externalOption)}
onFocus={() => setActiveOption(externalOption, false)}
shouldLookDisabled={isReset && selectedOptions.length === 0 && multiSelect}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
Expand Down
84 changes: 16 additions & 68 deletions code/core/src/components/components/Select/SelectOption.tsx
Comment thread
ghengeveld marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';

import { darken, transparentize } from 'polished';
import { styled } from 'storybook/theming';
import { ActionList } from '../..';

export interface SelectOptionProps {
/**
Expand All @@ -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;

Expand All @@ -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<SelectOptionProps> = ({
id,
title,
description,
icon,
aside,
children,
isSelected,
isActive,
Expand All @@ -111,7 +58,7 @@ export const SelectOption: React.FC<SelectOptionProps> = ({
...props
}) => {
return (
<Item
<ActionList.Item
{...props}
id={id}
role="option"
Expand All @@ -123,15 +70,16 @@ export const SelectOption: React.FC<SelectOptionProps> = ({
onKeyDown={onKeyDown}
>
{children ?? (
<Row>
{icon && <Icon>{icon}</Icon>}
<Col>
<Title>{title}</Title>
{description && <Description>{description}</Description>}
</Col>
</Row>
<>
{icon && <ActionList.Icon>{icon}</ActionList.Icon>}
<ActionList.Text>
<p>{title}</p>
{description && <small>{description}</small>}
</ActionList.Text>
{aside}
</>
)}
</Item>
</ActionList.Item>
);
};

Expand Down
1 change: 1 addition & 0 deletions code/core/src/components/components/Select/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Option {
title: string;
description?: string;
icon?: React.ReactNode;
aside?: React.ReactNode;
value: Value;
}
export interface InternalOption extends Omit<Option, 'value'> {
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/manager/components/layout/useDragging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
ghengeveld marked this conversation as resolved.
let draggedElement: typeof panelResizer | typeof sidebarResizer | null = null;

const onDragStart = (e: MouseEvent) => {
Expand Down
Loading
Loading