diff --git a/.changeset/segmented-control-subtle-add-view.md b/.changeset/segmented-control-subtle-add-view.md new file mode 100644 index 00000000000..d07d0845e84 --- /dev/null +++ b/.changeset/segmented-control-subtle-add-view.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +SegmentedControl: Add a subtle visual variant with group dividers and an optional trailing action child. \ No newline at end of file diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-linux.png new file mode 100644 index 00000000000..035f426850e Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-menu-linux.png new file mode 100644 index 00000000000..1102aa3f520 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-colorblind-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-linux.png new file mode 100644 index 00000000000..ba16b7b7fe2 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-menu-linux.png new file mode 100644 index 00000000000..35396e0147e Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-dimmed-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-linux.png new file mode 100644 index 00000000000..e820887aa34 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-menu-linux.png new file mode 100644 index 00000000000..d8299af781d Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-high-contrast-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-linux.png new file mode 100644 index 00000000000..035f426850e Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-menu-linux.png new file mode 100644 index 00000000000..1102aa3f520 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-linux.png new file mode 100644 index 00000000000..035f426850e Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-menu-linux.png new file mode 100644 index 00000000000..1102aa3f520 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-dark-tritanopia-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-linux.png new file mode 100644 index 00000000000..61e994b3dab Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-menu-linux.png new file mode 100644 index 00000000000..13bbfa89780 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-colorblind-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-linux.png new file mode 100644 index 00000000000..8daf776d16e Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-menu-linux.png new file mode 100644 index 00000000000..dc2e00b67cc Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-high-contrast-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-linux.png new file mode 100644 index 00000000000..61e994b3dab Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-menu-linux.png new file mode 100644 index 00000000000..13bbfa89780 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-linux.png new file mode 100644 index 00000000000..61e994b3dab Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-menu-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-menu-linux.png new file mode 100644 index 00000000000..13bbfa89780 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Narrow-Action-Menu-With-Action-light-tritanopia-menu-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-colorblind-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-colorblind-linux.png new file mode 100644 index 00000000000..d4fbf69e5ac Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-dimmed-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-dimmed-linux.png new file mode 100644 index 00000000000..9c00f856543 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-high-contrast-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-high-contrast-linux.png new file mode 100644 index 00000000000..a4870253c64 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-linux.png new file mode 100644 index 00000000000..d4fbf69e5ac Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-tritanopia-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-tritanopia-linux.png new file mode 100644 index 00000000000..d4fbf69e5ac Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-colorblind-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-colorblind-linux.png new file mode 100644 index 00000000000..679f711fa00 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-high-contrast-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-high-contrast-linux.png new file mode 100644 index 00000000000..ad7dbb56c2c Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-linux.png new file mode 100644 index 00000000000..679f711fa00 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-linux.png differ diff --git a/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-tritanopia-linux.png b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-tritanopia-linux.png new file mode 100644 index 00000000000..679f711fa00 Binary files /dev/null and b/.playwright/snapshots/components/SegmentedControl.test.ts-snapshots/SegmentedControl-Variant-Subtle-light-tritanopia-linux.png differ diff --git a/e2e/components/SegmentedControl.test.ts b/e2e/components/SegmentedControl.test.ts index beb82f849cb..5e854c5d2b7 100644 --- a/e2e/components/SegmentedControl.test.ts +++ b/e2e/components/SegmentedControl.test.ts @@ -28,10 +28,18 @@ const stories = [ title: 'Variant Narrow Action Menu', id: 'components-segmentedcontrol-features--variant-narrow-action-menu', }, + { + title: 'Variant Narrow Action Menu With Action', + id: 'components-segmentedcontrol-features--variant-narrow-action-menu-with-action', + }, { title: 'Variant Narrow Hide Labels', id: 'components-segmentedcontrol-features--variant-narrow-hide-labels', }, + { + title: 'Variant Subtle', + id: 'components-segmentedcontrol-features--variant-subtle', + }, { title: 'Controlled', id: 'components-segmentedcontrol-features--controlled', @@ -102,6 +110,12 @@ test.describe('SegmentedControl', () => { await page.keyboard.press('Shift+Tab') await expect(page).toHaveScreenshot(`SegmentedControl.${story.title}.${theme}.middle.selected.focus.png`) } + + if (story.title === 'Variant Narrow Action Menu With Action') { + await page.setViewportSize({width: viewports['primer.breakpoint.xs'], height: 768}) + await page.locator('[data-component="ActionMenu.Button"]').click() + await expect(page).toHaveScreenshot(`SegmentedControl.${story.title}.${theme}.menu.png`) + } }) }) } diff --git a/packages/react/src/SegmentedControl/SegmentedControl.docs.json b/packages/react/src/SegmentedControl/SegmentedControl.docs.json index b55d8b7e50f..58d75971776 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.docs.json +++ b/packages/react/src/SegmentedControl/SegmentedControl.docs.json @@ -19,6 +19,9 @@ { "id": "components-segmentedcontrol-features--variant-narrow-action-menu" }, + { + "id": "components-segmentedcontrol-features--variant-narrow-action-menu-with-action" + }, { "id": "components-segmentedcontrol-features--fullwidth-narrow" }, @@ -33,6 +36,9 @@ }, { "id": "components-segmentedcontrol-features--associated-with-a-label-and-caption" + }, + { + "id": "components-segmentedcontrol-features--variant-subtle" } ], "importPath": "@primer/react", @@ -69,9 +75,9 @@ }, { "name": "variant", - "type": "| 'default' | { narrow?: 'hideLabels' | 'dropdown' | 'default' regular?: 'hideLabels' | 'dropdown' | 'default' wide?: 'hideLabels' | 'dropdown' | 'default' }", + "type": "| 'default' | 'subtle' | { narrow?: 'hideLabels' | 'dropdown' | 'default' | 'subtle' regular?: 'hideLabels' | 'dropdown' | 'default' | 'subtle' wide?: 'hideLabels' | 'dropdown' | 'default' | 'subtle' }", "defaultValue": "'default'", - "description": "Configure alternative ways to render the control when it gets rendered in tight spaces" + "description": "Configure the control's visual treatment or responsive behavior in tight spaces." }, { "name": "size", @@ -177,6 +183,53 @@ "defaultValue": "" } ] + }, + { + "name": "SegmentedControl.Divider", + "props": [ + { + "name": "className", + "type": "string", + "required": false + }, + { + "name": "style", + "type": "React.CSSProperties", + "required": false + } + ] + }, + { + "name": "SegmentedControl.Action", + "props": [ + { + "name": "label", + "type": "string", + "required": true, + "description": "Accessible label for the action icon button." + }, + { + "name": "icon", + "type": "Component", + "required": true, + "description": "The icon rendered by the action button." + }, + { + "name": "onClick", + "type": "() => void", + "description": "The handler that gets called when the action button is clicked." + }, + { + "name": "disabled", + "type": "boolean", + "description": "Whether the action is disabled." + }, + { + "name": "loading", + "type": "boolean", + "description": "Whether the action is in a loading state." + } + ] } ] -} \ No newline at end of file +} diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.module.css b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.module.css index 8ebf0ef2aad..d578fa504b6 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.module.css @@ -4,6 +4,10 @@ gap: var(--base-size-4); } +.ResetButton { + margin-top: var(--base-size-24); +} + @media screen and (min-width: 768px) { .LabelAndCaptionContainer { flex-direction: row; diff --git a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx index a4d52c079f2..1893aee05a7 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.features.stories.tsx @@ -1,7 +1,8 @@ import {useState} from 'react' import type {Meta} from '@storybook/react-vite' +import {PlusIcon, EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' import {SegmentedControl} from '.' -import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' +import {Button} from '../Button' import Text from '../Text' import classes from './SegmentedControl.features.stories.module.css' @@ -34,6 +35,20 @@ export const WithCounterLabels = () => ( ) +export const VariantSubtle = () => ( + + + All + + + Active + Review requests + + Done + +) +VariantSubtle.storyName = '[variant: subtle] Low emphasis' + export const Controlled = () => { const [selectedIndex, setSelectedIndex] = useState(0) const handleChange = (i: number) => { @@ -78,6 +93,56 @@ export const VariantNarrowActionMenu = () => ( ) VariantNarrowActionMenu.storyName = '[variant: narrow] Action menu' +export const VariantNarrowActionMenuWithAction = () => { + const initialViews = [ + {id: 'all', label: 'All'}, + {id: 'active', label: 'Active'}, + {id: 'review-requests', label: 'Review requests'}, + {id: 'done', label: 'Done'}, + ] + const [views, setViews] = useState(initialViews) + const [selectedViewId, setSelectedViewId] = useState(initialViews[0].id) + + const handleAddView = () => { + setViews(currentViews => { + const viewNumber = currentViews.length - 3 + return [...currentViews, {id: `new-view-${viewNumber}`, label: `New view ${viewNumber}`}] + }) + } + + const handleChange = (index: number) => { + const nextSelectedViewId = views.at(index)?.id + if (nextSelectedViewId) setSelectedViewId(nextSelectedViewId) + } + + const handleReset = () => { + setViews(initialViews) + setSelectedViewId(initialViews[0].id) + } + + return ( + <> + + {views.flatMap(view => [ + ...(view.label === 'Active' ? [] : []), + + {view.label} + , + ])} + + + + + ) +} +VariantNarrowActionMenuWithAction.storyName = '[variant: narrow] Action menu with trailing action' + export const FullwidthNarrow = () => ( diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index b4567b1ba85..aa7c5703d87 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,3 +1,100 @@ +@define-mixin subtleVariant { + --segmented-control-outer-radius: var(--borderRadius-large); + --segmented-control-button-bg-inset: 0px; + + background-color: transparent; + border: 0; + + /* stylelint-disable-next-line nesting-selector-no-missing-scoping-root */ + &::before { + display: none; + } + + .Item:not(:last-child)::after { + background-color: transparent; + } + + > [data-component='SegmentedControl.Divider'] { + display: block; + } + + .Button { + padding: 0; + color: var(--fgColor-muted); + border: 0; + border-radius: var(--borderRadius-large); + + .Content { + padding-inline: var(--base-size-12); + border: 0; + } + } + + .Button[aria-pressed='true'] { + color: var(--fgColor-default); + + .Content { + background-color: var(--bgColor-muted); + } + } + + .Button:where(:not([aria-pressed='true'], [aria-disabled='true'])):hover .Content { + background-color: var(--bgColor-muted); + background-color: color-mix(in srgb, var(--bgColor-muted) 50%, transparent); + } +} + +@define-mixin defaultVariant { + --segmented-control-outer-radius: var(--borderRadius-medium); + --segmented-control-button-bg-inset: 4px; + + background-color: var(--controlTrack-bgColor-rest); + border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent); + + /* stylelint-disable-next-line nesting-selector-no-missing-scoping-root */ + &::before { + display: block; + } + + .Item:not(:last-child)::after { + /* stylelint-disable-next-line primer/colors */ + background-color: var(--borderColor-default); + } + + > [data-component='SegmentedControl.Divider'] { + display: none; + } + + .Button { + /* stylelint-disable-next-line primer/spacing */ + padding: var(--segmented-control-button-bg-inset); + color: currentColor; + /* stylelint-disable-next-line primer/borders */ + border-radius: var(--segmented-control-outer-radius); + + .Content { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); + border: var(--borderWidth-thin) solid transparent; + } + } + + .Button[aria-pressed='true'] { + padding: 0; + color: currentColor; + + .Content { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--segmented-control-button-inner-padding); + background-color: transparent; + } + } + + .Button:where(:not([aria-pressed='true'], [aria-disabled='true'])):hover .Content { + background-color: var(--controlTrack-bgColor-hover); + } +} + .SegmentedControl { /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ --segmented-control-icon-width: 32px; @@ -30,6 +127,10 @@ transition: inset var(--base-duration-200) var(--base-easing-easeInOut); } } + + &[data-disable-knob-transition]::before { + transition: none; + } } /* Responsive full-width */ @@ -47,6 +148,19 @@ --segmented-control-icon-width: 32px; } + > [data-component='SegmentedControl.Divider'] { + display: none; + align-self: stretch; + width: 1px; + margin-block: var(--base-size-8); + margin-inline-start: var(--base-size-8); + /* stylelint-disable-next-line primer/spacing -- preserve the previous divider's 16px layout footprint */ + margin-inline-end: calc(var(--base-size-8) - 1px); + list-style: none; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--borderColor-default); + } + @media (--viewportRange-narrow) { &[data-full-width-narrow='true'] { display: flex; @@ -107,6 +221,10 @@ display: none; } + &:where([data-variant='subtle']) { + @mixin subtleVariant; + } + /* Handle hideLabels variant - hide button text */ &[data-variant='hideLabels'] .Text { display: none; @@ -120,6 +238,10 @@ &[data-variant-narrow='hideLabels'] .Text { display: none; } + + &:where([data-variant-narrow='subtle']) { + @mixin subtleVariant; + } } @media (--viewportRange-regular) { @@ -130,6 +252,10 @@ &[data-variant-regular='hideLabels'] .Text { display: none; } + + &:where([data-variant-regular='subtle']) { + @mixin subtleVariant; + } } @media (--viewportRange-wide) { @@ -140,6 +266,14 @@ &[data-variant-wide='hideLabels'] .Text { display: none; } + + &:where([data-variant-wide='subtle']) { + @mixin subtleVariant; + } + + &:where([data-variant-regular='subtle']):not([data-variant-wide='subtle']) { + @mixin defaultVariant; + } } &:where([data-size='small']) { @@ -149,6 +283,102 @@ } } +.DropdownGroup { + --segmented-control-action-size: 32px; + --segmented-control-list-flex: 0 1 auto; + + display: inline-flex; + align-items: center; + gap: var(--base-size-4); + + &[data-full-width='true'] { + width: 100%; + + --segmented-control-list-flex: 1 1 auto; + } + + &[data-full-width='false'] { + width: auto; + } + + @media (--viewportRange-narrow) { + &[data-full-width-narrow='true'] { + width: 100%; + + --segmented-control-list-flex: 1 1 auto; + } + + &[data-full-width-narrow='false'] { + width: auto; + } + } + + @media (--viewportRange-regular) { + &[data-full-width-regular='true'] { + width: 100%; + + --segmented-control-list-flex: 1 1 auto; + } + + &[data-full-width-regular='false'] { + width: auto; + } + } + + @media (--viewportRange-wide) { + &[data-full-width-wide='true'] { + width: 100%; + + --segmented-control-list-flex: 1 1 auto; + } + + &[data-full-width-wide='false'] { + width: auto; + } + + &[data-full-width-regular='true']:not([data-full-width-wide='true']) { + width: auto; + + --segmented-control-list-flex: 0 1 auto; + } + } + + &[data-size='small'] { + --segmented-control-action-size: 28px; + } + + > .SegmentedControl { + min-width: 0; + flex: var(--segmented-control-list-flex); + } +} + +.InlineAction { + display: flex; + + &[data-variant='dropdown'] { + display: none; + } + + @media (--viewportRange-narrow) { + &[data-variant-narrow='dropdown'] { + display: none; + } + } + + @media (--viewportRange-regular) { + &[data-variant-regular='dropdown'] { + display: none; + } + } + + @media (--viewportRange-wide) { + &[data-variant-wide='dropdown'] { + display: none; + } + } +} + .DropdownContainer { display: none; @@ -202,6 +432,8 @@ /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ &:has(+ [data-selected])::after, + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- account for a hidden divider in the default variant */ + &:has(+ :where([data-component='SegmentedControl.Divider']) + :where([data-selected]))::after, &:where([data-selected])::after { background-color: transparent; } @@ -224,6 +456,18 @@ } } +.ActionItem { + display: flex; + align-items: center; + margin-inline-start: 0; +} + +.ActionButton { + width: var(--segmented-control-action-size); + height: var(--segmented-control-action-size); + padding: 0; +} + .Button { /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ --segmented-control-button-inner-padding: 12px; diff --git a/packages/react/src/SegmentedControl/SegmentedControl.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.stories.tsx index 3ba22244995..a19d6d93e8a 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.stories.tsx @@ -2,29 +2,33 @@ import type {StoryFn, Meta} from '@storybook/react-vite' import {SegmentedControl} from '.' import SegmentedControlIconButton from './SegmentedControlIconButton' import SegmentedControlButton from './SegmentedControlButton' -import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' +import {EyeIcon, FileCodeIcon, PeopleIcon, PlusIcon} from '@primer/octicons-react' -type ResponsiveVariantOptions = 'dropdown' | 'hideLabels' | 'default' +type ResponsiveVariantOptions = 'dropdown' | 'hideLabels' | 'default' | 'subtle' type Args = { - fullWidth?: boolean - fullWidthAtNarrow?: boolean - fullWidthAtRegular?: boolean - fullWidthAtWide?: boolean size?: 'small' | 'medium' + variant: 'default' | 'subtle' variantAtNarrow: ResponsiveVariantOptions variantAtRegular: ResponsiveVariantOptions variantAtWide: ResponsiveVariantOptions + fullWidth?: boolean + fullWidthAtNarrow?: boolean + fullWidthAtRegular?: boolean + fullWidthAtWide?: boolean + showDivider?: boolean + showAction?: boolean } -const excludedControlKeys = ['variant'] +const variantOptions = ['dropdown', 'hideLabels', 'default', 'subtle'] -const variantOptions = ['dropdown', 'hideLabels', 'default'] - -const parseVariantFromArgs = ({variantAtNarrow, variantAtRegular, variantAtWide}: Args) => ({ - narrow: variantAtNarrow, - regular: variantAtRegular, - wide: variantAtWide, -}) +const parseVariantFromArgs = ({variant, variantAtNarrow, variantAtRegular, variantAtWide}: Args) => + variant === 'subtle' + ? variant + : { + narrow: variantAtNarrow, + regular: variantAtRegular, + wide: variantAtWide, + } const parseFullWidthFromArgs = ({fullWidth, fullWidthAtNarrow, fullWidthAtRegular, fullWidthAtWide}: Args) => fullWidth @@ -40,16 +44,52 @@ export default { component: SegmentedControl, subcomponents: {SegmentedControlButton, SegmentedControlIconButton}, args: { - fullWidth: false, - fullWidthAtNarrow: false, - fullWidthAtRegular: false, - fullWidthAtWide: false, size: 'medium', + variant: 'default', variantAtNarrow: 'default', variantAtRegular: 'default', variantAtWide: 'default', + fullWidth: false, + fullWidthAtNarrow: false, + fullWidthAtRegular: false, + fullWidthAtWide: false, + showDivider: false, + showAction: false, }, argTypes: { + size: { + control: { + type: 'radio', + }, + options: ['small', 'medium'], + }, + variant: { + control: { + type: 'radio', + }, + options: ['default', 'subtle'], + }, + variantAtNarrow: { + name: 'variant.narrow', + control: { + type: 'radio', + }, + options: variantOptions, + }, + variantAtRegular: { + name: 'variant.regular', + control: { + type: 'radio', + }, + options: variantOptions, + }, + variantAtWide: { + name: 'variant.wide', + control: { + type: 'radio', + }, + options: variantOptions, + }, fullWidth: { control: { type: 'boolean', @@ -73,35 +113,19 @@ export default { type: 'boolean', }, }, - size: { - control: { - type: 'radio', - }, - options: ['small', 'medium'], - }, - variantAtNarrow: { - name: 'variant.narrow', - control: { - type: 'radio', - }, - options: variantOptions, - }, - variantAtRegular: { - name: 'variant.regular', + showDivider: { control: { - type: 'radio', + type: 'boolean', }, - options: variantOptions, + name: 'divider', }, - variantAtWide: { - name: 'variant.wide', + showAction: { control: { - type: 'radio', + type: 'boolean', }, - options: variantOptions, + name: 'action', }, }, - parameters: {controls: {exclude: excludedControlKeys}}, } as Meta export const Playground: StoryFn = args => ( @@ -114,12 +138,14 @@ export const Playground: StoryFn = args => ( Preview + {args.showDivider && } Raw Blame + {args.showAction && undefined} />} ) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx index c0f6ff4f045..d0fd3e82d3e 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx @@ -1,10 +1,11 @@ -import {render, fireEvent, waitFor} from '@testing-library/react' -import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' +import {act, render, fireEvent, waitFor} from '@testing-library/react' +import {EyeIcon, FileCodeIcon, PeopleIcon, PlusIcon} from '@primer/octicons-react' import userEvent from '@testing-library/user-event' import {describe, expect, it, vi} from 'vitest' import BaseStyles from '../BaseStyles' import {SegmentedControl} from '../SegmentedControl' import {implementsClassName} from '../utils/testing' +import {asSlot} from '../utils/as-slot' import classes from './SegmentedControl.module.css' const segmentData = [ @@ -46,6 +47,29 @@ describe('SegmentedControl', () => { expect(getByRole('list')).toHaveAttribute('data-component', 'SegmentedControl') }) + it('renders the subtle variant', () => { + const {getByRole} = render( + + Preview + , + ) + + expect(getByRole('list')).toHaveAttribute('data-variant', 'subtle') + }) + + it('renders the subtle variant responsively', () => { + const {container} = render( + + Preview + , + ) + const list = container.querySelector('[data-component="SegmentedControl"]') + + expect(list).toHaveAttribute('data-variant-narrow', 'dropdown') + expect(list).toHaveAttribute('data-variant-regular', 'subtle') + expect(list).toHaveAttribute('data-variant-wide', 'subtle') + }) + it('renders data-component attribute on segmented control buttons', () => { const {getByRole} = render( @@ -59,6 +83,43 @@ describe('SegmentedControl', () => { ) }) + it('renders dividers without including them in selection indices', async () => { + const onChange = vi.fn() + const {container, getByRole} = render( + + Preview + + Raw + , + ) + + expect(container.querySelector('[data-component="SegmentedControl.Divider"]')).toBeInTheDocument() + await userEvent.click(getByRole('button', {name: 'Raw'})) + expect(onChange).toHaveBeenCalledWith(1) + }) + + it('renders wrapped dividers in the segmented control and dropdown', async () => { + const onChange = vi.fn() + const WrappedDivider = asSlot(() => , SegmentedControl.Divider) + const {container, getAllByRole} = render( + + Preview + + Raw + , + ) + + expect(container.querySelector('[data-component="SegmentedControl.Divider"]')).toBeInTheDocument() + await userEvent.click(document.querySelector('[data-component="ActionMenu.Button"]')!) + expect(document.querySelector('[data-component="ActionList.Divider"]')).toBeInTheDocument() + await userEvent.click(getAllByRole('menuitemradio')[1]) + expect(onChange).toHaveBeenCalledWith(1) + }) + it('renders data-component attribute on segmented control icon buttons', () => { const {getByRole} = render( @@ -340,6 +401,124 @@ describe('SegmentedControl', () => { expect(handleClick).toHaveBeenCalled() }) + it('renders the action as a labeled menu item after a divider in the dropdown', async () => { + const handleAddView = vi.fn() + const component = render( + + + Preview + Raw + + + , + ) + + fireEvent.click(component.getByRole('button', {name: 'Preview, File view'})) + + const menuAction = await waitFor(() => component.getByRole('menuitem', {name: 'Add view'})) + expect(menuAction.closest('[data-component="ActionList.Item"]')?.previousElementSibling).toHaveAttribute( + 'data-component', + 'ActionList.Divider', + ) + expect(menuAction).toHaveAttribute('data-component', 'SegmentedControl.Action') + + fireEvent.click(menuAction) + + expect(handleAddView).toHaveBeenCalledExactlyOnceWith() + expect(component.getByRole('menu')).toBeInTheDocument() + }) + + it('renders a wrapped action slot in the dropdown', async () => { + const handleAddView = vi.fn() + const AddViewAction = asSlot( + () => , + SegmentedControl.Action, + ) + const component = render( + + + Preview + + + , + ) + + fireEvent.click(component.getByRole('button', {name: 'Preview, File view'})) + fireEvent.click(await component.findByRole('menuitem', {name: 'Add view'})) + + expect(handleAddView).toHaveBeenCalledOnce() + }) + + it('applies responsive full-width attributes to the layout wrapper', () => { + const {getByRole} = render( + + Preview + + , + ) + + expect(getByRole('list').parentElement).toHaveAttribute('data-full-width-narrow', 'true') + expect(getByRole('list').parentElement).toHaveAttribute('data-full-width-regular', 'false') + }) + + it('calls the action from the icon action button', async () => { + const user = userEvent.setup() + const handleAddView = vi.fn() + const {getByRole} = render( + + + + All + + Active + Review requests + + + , + ) + + const iconActionButton = getByRole('button', {name: 'Add view'}) + + expect(iconActionButton.parentElement?.closest('[data-component]')).toHaveAttribute( + 'data-component', + 'SegmentedControl.Action', + ) + + await user.click(iconActionButton) + + expect(handleAddView).toHaveBeenCalledExactlyOnceWith() + }) + + it('disables the knob transition while the number of rendered segments changes', () => { + let animationFrameCallback: FrameRequestCallback | undefined + const requestAnimationFrameSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation(callback => { + animationFrameCallback = callback + return 1 + }) + const {getByRole, rerender} = render( + + All + Active + {false} + , + ) + + rerender( + + All + Active + New view + , + ) + + expect(getByRole('list')).toHaveAttribute('data-disable-knob-transition', '') + + act(() => animationFrameCallback?.(0)) + + expect(getByRole('list')).not.toHaveAttribute('data-disable-knob-transition') + requestAnimationFrameSpy.mockRestore() + }) + it('supports deprecated leadingIcon prop for backward compatibility', () => { const {getByText} = render( diff --git a/packages/react/src/SegmentedControl/SegmentedControl.tsx b/packages/react/src/SegmentedControl/SegmentedControl.tsx index 707530b01f8..c472b36f367 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.tsx @@ -3,15 +3,20 @@ import type {SegmentedControlButtonProps} from './SegmentedControlButton' import Button from './SegmentedControlButton' import type {SegmentedControlIconButtonProps} from './SegmentedControlIconButton' import SegmentedControlIconButton from './SegmentedControlIconButton' +import Action from './SegmentedControlAction' +import Divider from './SegmentedControlDivider' +import {SegmentedControlActionContext} from './SegmentedControlActionContext' import {ActionList} from '../ActionList' import {ActionMenu} from '../ActionMenu' import type {ResponsiveValue} from '../hooks/useResponsiveValue' +import {useSlots} from '../hooks/useSlots' import {getResponsiveAttributes} from '../internal/utils/getResponsiveAttributes' import type {WidthOnlyViewportRangeKeys} from '../utils/types/ViewportRangeKeys' import {isElement} from 'react-is' import classes from './SegmentedControl.module.css' import {clsx} from 'clsx' import {isSlot} from '../utils/is-slot' +import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect' export type SegmentedControlProps = { 'aria-label'?: string @@ -24,7 +29,10 @@ export type SegmentedControlProps = { /** The size of the buttons */ size?: 'small' | 'medium' /** Configure alternative ways to render the control when it gets rendered in tight spaces */ - variant?: 'default' | Partial> + variant?: + | 'default' + | 'subtle' + | Partial> className?: string } @@ -40,13 +48,36 @@ const Root: React.FC> = ({ ...rest }) => { const segmentedControlContainerRef = useRef(null) + const [slots, segmentChildren] = useSlots(children, {action: Action}) + const actionChild = slots.action + const segments = segmentChildren.filter( + child => React.isValidElement(child) && !isSlot(child, Divider), + ) as React.ReactElement[] + const segmentIndices = new Map(segments.map((segment, index) => [segment, index])) + const segmentCount = segments.length + const previousSegmentCount = useRef(segmentCount) + + useIsomorphicLayoutEffect(() => { + if (previousSegmentCount.current === segmentCount) return + + const segmentedControl = segmentedControlContainerRef.current + segmentedControl?.setAttribute('data-disable-knob-transition', '') + previousSegmentCount.current = segmentCount + + const animationFrame = requestAnimationFrame(() => { + segmentedControl?.removeAttribute('data-disable-knob-transition') + }) + + return () => cancelAnimationFrame(animationFrame) + }, [segmentCount]) + const isUncontrolled = onChange === undefined || - React.Children.toArray(children).some( + segments.some( child => React.isValidElement(child) && child.props.defaultSelected !== undefined, ) - const selectedSegments = React.Children.toArray(children).map( + const selectedSegments = segments.map( child => React.isValidElement(child) && (child.props.defaultSelected || child.props.selected), @@ -56,12 +87,11 @@ const Root: React.FC> = ({ const [selectedIndexInternalState, setSelectedIndexInternalState] = useState(selectedIndexExternal) const selectedIndex = isUncontrolled ? selectedIndexInternalState : selectedIndexExternal const selectedChild = React.isValidElement( - React.Children.toArray(children)[selectedIndex], + segments[selectedIndex], ) - ? React.Children.toArray(children)[selectedIndex] + ? segments[selectedIndex] : undefined - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getChildIcon = (childArg: React.ReactNode): React.ReactElement | null => { + const getChildIcon = (childArg: React.ReactNode): React.ReactElement | null => { if ( React.isValidElement(childArg) && (childArg.type === Button || isSlot(childArg, Button)) @@ -69,12 +99,10 @@ const Root: React.FC> = ({ // Use leadingVisual if provided, otherwise fall back to leadingIcon for backwards compatibility const leadingVisual = childArg.props.leadingVisual ?? childArg.props.leadingIcon if (leadingVisual) { - if (isElement(leadingVisual)) { - return leadingVisual - } else { - const LeadingVisual = leadingVisual - return - } + if (isElement(leadingVisual)) return leadingVisual + + const LeadingVisual = leadingVisual + return } } @@ -82,12 +110,10 @@ const Root: React.FC> = ({ React.isValidElement(childArg) && (childArg.type === SegmentedControlIconButton || isSlot(childArg, SegmentedControlIconButton)) ) { - if (isElement(childArg.props.icon)) { - childArg.props.icon - } else { - const Icon = childArg.props.icon - return - } + if (isElement(childArg.props.icon)) return childArg.props.icon + + const Icon = childArg.props.icon + return } return null @@ -130,20 +156,31 @@ const Root: React.FC> = ({ - {React.Children.map(children, (child, index) => { + {segmentChildren.map((child, index) => { + if (React.isValidElement(child) && isSlot(child, Divider)) { + return ( + + ) + } + const ChildIcon = getChildIcon(child) // Not a valid child element - skip rendering if (!React.isValidElement(child)) { return null } + const segmentIndex = segmentIndices.get(child) ?? -1 return ( { - isUncontrolled && setSelectedIndexInternalState(index) - onChange && onChange(index) + isUncontrolled && setSelectedIndexInternalState(segmentIndex) + onChange && onChange(segmentIndex) child.props.onClick && child.props.onClick(event as React.MouseEvent) }} > @@ -151,6 +188,11 @@ const Root: React.FC> = ({ ) })} + {actionChild && ( + + {actionChild} + + )} @@ -170,11 +212,16 @@ const Root: React.FC> = ({ {...rest} data-component="SegmentedControl" > - {React.Children.map(children, (child, index) => { + {segmentChildren.map((child, index) => { + if (React.isValidElement(child) && isSlot(child, Divider)) { + return React.cloneElement(child, {key: child.key ?? `segmented-control-divider-${index}`}) + } + // Not a valid child element - skip rendering child if (!React.isValidElement(child)) { return null } + const segmentIndex = segmentIndices.get(child) ?? -1 const sharedChildProps = { onClick: onChange ? (event: React.MouseEvent) => { @@ -183,8 +230,8 @@ const Root: React.FC> = ({ child.props['aria-disabled'] === 'true' || child.props['aria-disabled'] === true if (!isDisabled) { - onChange(index) - isUncontrolled && setSelectedIndexInternalState(index) + onChange(segmentIndex) + isUncontrolled && setSelectedIndexInternalState(segmentIndex) child.props.onClick && child.props.onClick(event) } } @@ -195,29 +242,37 @@ const Root: React.FC> = ({ child.props['aria-disabled'] === true if (!isDisabled) { child.props.onClick && child.props.onClick(event) - isUncontrolled && setSelectedIndexInternalState(index) + isUncontrolled && setSelectedIndexInternalState(segmentIndex) } }, - selected: index === selectedIndex, + selected: segmentIndex === selectedIndex, style: { '--separator-color': - index === selectedIndex || index === selectedIndex - 1 ? 'transparent' : 'var(--borderColor-default)', + segmentIndex === selectedIndex || segmentIndex === selectedIndex - 1 + ? 'transparent' + : 'var(--borderColor-default)', ...child.props.style, }, } // Render the children as-is and add the shared child props - return React.cloneElement(child, sharedChildProps) + return React.cloneElement(child, {key: child.key ?? `segmented-control-item-${index}`, ...sharedChildProps}) })} ) - // Return both variants when dropdown is used, otherwise just the segmented control - return hasDropdownVariant ? ( - <> + const inlineActionContent = actionChild && ( +
+ {actionChild} +
+ ) + + return hasDropdownVariant || actionChild ? ( +
{dropdownContent} {segmentedControlContent} - + {inlineActionContent} +
) : ( segmentedControlContent ) @@ -228,4 +283,6 @@ Root.displayName = 'SegmentedControl' export const SegmentedControl = Object.assign(Root, { Button, IconButton: SegmentedControlIconButton, + Divider, + Action, }) diff --git a/packages/react/src/SegmentedControl/SegmentedControlAction.tsx b/packages/react/src/SegmentedControl/SegmentedControlAction.tsx new file mode 100644 index 00000000000..9c4da2f8d4f --- /dev/null +++ b/packages/react/src/SegmentedControl/SegmentedControlAction.tsx @@ -0,0 +1,82 @@ +import type React from 'react' +import {useContext} from 'react' +import type {ElementType} from 'react' +import {ActionList} from '../ActionList' +import {IconButton} from '../Button' +import type {FCWithSlotMarker} from '../utils/types' +import classes from './SegmentedControl.module.css' +import {SegmentedControlActionContext} from './SegmentedControlActionContext' + +export interface SegmentedControlActionProps { + label: string + icon: ElementType + onClick?: () => void + disabled?: boolean + loading?: boolean +} + +interface SegmentedControlMenuActionProps { + children?: React.ReactNode + onClick?: (event: React.MouseEvent) => void + onKeyPress?: (event: React.KeyboardEvent) => void + 'aria-disabled'?: boolean + 'aria-labelledby'?: string + 'aria-describedby'?: string + className?: string + role?: string + tabIndex?: number +} + +const SegmentedControlMenuAction: React.FC = ({children, ...props}) => ( +
+ {children} +
+) + +const SegmentedControlAction: FCWithSlotMarker = props => { + const {disabled, icon: Icon, label, loading, onClick} = props + const variant = useContext(SegmentedControlActionContext) + + if (variant === 'menu') { + return ( + <> + + { + event.preventDefault() + onClick?.() + }} + > + + + + {label} + + + ) + } + + return ( +
+ onClick?.()} + variant="invisible" + /> +
+ ) +} + +SegmentedControlAction.displayName = 'SegmentedControl.Action' + +export default SegmentedControlAction + +SegmentedControlAction.__SLOT__ = Symbol('SegmentedControl.Action') diff --git a/packages/react/src/SegmentedControl/SegmentedControlActionContext.ts b/packages/react/src/SegmentedControl/SegmentedControlActionContext.ts new file mode 100644 index 00000000000..0fec8d0d4d6 --- /dev/null +++ b/packages/react/src/SegmentedControl/SegmentedControlActionContext.ts @@ -0,0 +1,3 @@ +import {createContext} from 'react' + +export const SegmentedControlActionContext = createContext<'inline' | 'menu'>('inline') diff --git a/packages/react/src/SegmentedControl/SegmentedControlDivider.tsx b/packages/react/src/SegmentedControl/SegmentedControlDivider.tsx new file mode 100644 index 00000000000..182f3269589 --- /dev/null +++ b/packages/react/src/SegmentedControl/SegmentedControlDivider.tsx @@ -0,0 +1,16 @@ +import type React from 'react' +import type {FCWithSlotMarker} from '../utils/types' + +export type SegmentedControlDividerProps = { + className?: string + style?: React.CSSProperties +} + +const SegmentedControlDivider: FCWithSlotMarker = ({className, style}) => ( +