Skip to content
92 changes: 91 additions & 1 deletion examples/bpk-component-segmented-control/examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@
* limitations under the License.
*/

import { useState } from 'react';

import {
canvasContrastDay,
surfaceContrastDay,
} from '@skyscanner/bpk-foundations-web/tokens/base.es6';

import BpkSegmentedControl from '../../packages/bpk-component-segmented-control';
import BpkSegmentedControl, {
useSegmentedControlPanels,
} from '../../packages/bpk-component-segmented-control';
import { SEGMENT_TYPES } from '../../packages/bpk-component-segmented-control/src/BpkSegmentedControl';
import BpkText from '../../packages/bpk-component-text';
import { cssModules } from '../../packages/bpk-react-utils';
// @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
import { BpkDarkExampleWrapper } from '../bpk-storybook-utils';
Expand Down Expand Up @@ -276,6 +281,89 @@ const VisualExample = () => (
</>
);

// Example demonstrating the recommended hook pattern for managing tabs and panels.
// The hook automatically handles ID generation and ARIA relationships.
const WithHookControlledPanelsExample = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
const buttonContents = ['Flights', 'Hotels', 'Car hire'];

const { controlProps, getPanelProps } = useSegmentedControlPanels(
buttonContents,
selectedIndex,
);

const panelStyle = {
padding: '1rem',
border: '1px solid #ddd',
borderRadius: '0.5rem',
marginTop: '1rem',
};

return (
<div>
<BpkSegmentedControl
{...controlProps}
label="Travel options"
onItemClick={setSelectedIndex}
type={SEGMENT_TYPES.CanvasDefault}
/>
<div
{...getPanelProps(0)}
style={panelStyle}
>
<BpkText>Search for flights to your destination.</BpkText>
</div>
<div
{...getPanelProps(1)}
style={panelStyle}
>
<BpkText>Find the perfect place to stay.</BpkText>
</div>
<div
{...getPanelProps(2)}
style={panelStyle}
>
<BpkText>Rent a car for your trip.</BpkText>
</div>
</div>
);
};

// Example using conditional rendering instead of the hidden attribute.
// Both approaches are valid - use whichever fits your use case better.
const WithConditionalPanelsExample = () => {
const [selectedIndex, setSelectedIndex] = useState(0);

const panelStyle = {
padding: '1rem',
border: '1px solid #ddd',
borderRadius: '0.5rem',
marginTop: '1rem',
};

return (
<div>
<BpkSegmentedControl
buttonContents={['Specific dates', 'Flexible dates']}
label="Date selection mode"
onItemClick={setSelectedIndex}
selectedIndex={selectedIndex}
type={SEGMENT_TYPES.SurfaceDefault}
/>
{selectedIndex === 0 && (
<div role="tabpanel" style={panelStyle}>
<BpkText>Specific dates Panel</BpkText>
</div>
)}
{selectedIndex === 1 && (
<div role="tabpanel" style={panelStyle}>
<BpkText>Flexible dates Panel</BpkText>
</div>
)}
</div>
);
};

export {
SimpleDefault,
SimpleCanvasContrast,
Expand All @@ -292,4 +380,6 @@ export {
ComplexCanvasDefault,
ComplexSurfaceDefaultNoShadow,
VisualExample,
WithHookControlledPanelsExample,
WithConditionalPanelsExample,
};
4 changes: 4 additions & 0 deletions examples/bpk-component-segmented-control/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
ComplexCanvasDefault,
ComplexSurfaceDefaultNoShadow,
VisualExample,
WithHookControlledPanelsExample,
WithConditionalPanelsExample,
} from './examples';

export default {
Expand All @@ -57,6 +59,8 @@ export const ComplexThreeSegmentsCanvasContrast = ComplexCanvasContrast;
export const ComplexThreeSegmentsCanvasDefault = ComplexCanvasDefault;
export const ComplexThreeSegmentsSurfaceDefaultNoShadow =
ComplexSurfaceDefaultNoShadow;
export const WithHookControlledPanels = WithHookControlledPanelsExample;
export const WithConditionalPanels = WithConditionalPanelsExample;
export const VisualTest = VisualExample;
export const VisualTestWithZoom = {
render: VisualTest,
Expand Down
91 changes: 80 additions & 11 deletions packages/bpk-component-segmented-control/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,93 @@
# bpk-segmented-control
# bpk-component-segmented-control

> Backpack segmented control component.

## Installation

Check the main [Readme](https://github.com/skyscanner/backpack#usage) for a complete installation guide.

## Usage
```js
import BpkSegmentedControl from '@skyscanner/backpack-web/bpk-component-segmented-control';

### Basic Usage

```tsx
import BpkSegmentedControl, {
SEGMENT_TYPES,
} from '@skyscanner/backpack-web/bpk-component-segmented-control';

export default () => (
<BpkSegmentedControl
buttonContents={buttonContent}
label='Trip type' // Accessible name, this should be localised
onItemClick={() => {}}
selectedIndex={1} // button selected on load
type={SEGMENT_TYPES.SurfaceContrast}
shadow
buttonContents={['Specific dates', 'Flexible dates']}
label="Date selection" // Accessible name, this should be localised
onItemClick={(index) => console.log(`Selected index: ${index}`)}
selectedIndex={0}
type={SEGMENT_TYPES.CanvasDefault}
/>
)
);
```

### With Tab Panels (Recommended)

When using the segmented control to switch between content panels, use the `useSegmentedControlPanels` hook for automatic ID generation and proper ARIA relationships.

```tsx
import { useState } from 'react';
import BpkSegmentedControl, {
useSegmentedControlPanels,
SEGMENT_TYPES,
} from '@skyscanner/backpack-web/bpk-component-segmented-control';

const TabbedContent = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
const buttonContents = ['Flights', 'Hotels', 'Car hire'];

const { controlProps, getPanelProps } = useSegmentedControlPanels(
buttonContents,
selectedIndex,
);

return (
<div>
<BpkSegmentedControl
{...controlProps}
label="Travel options"
onItemClick={setSelectedIndex}
type={SEGMENT_TYPES.CanvasDefault}
/>
<div {...getPanelProps(0)}>
<p>Search for flights to your destination.</p>
</div>
<div {...getPanelProps(1)}>
<p>Find the perfect place to stay.</p>
</div>
<div {...getPanelProps(2)}>
<p>Rent a car for your trip.</p>
</div>
</div>
);
};

export default TabbedContent;
```

## Accessibility

The `BpkSegmentedControl` component implements the [ARIA tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) with full keyboard navigation support.

### Keyboard Navigation

- **Arrow Left/Right**: Move focus between tabs (respects RTL layouts)
- **Home**: Move focus to the first tab
- **End**: Move focus to the last tab
- **Enter/Space**: Activate the focused tab (in manual mode)

### Activation Modes

The component supports two activation modes:

- **Automatic (default)**: Tabs are activated automatically when focused via keyboard navigation. This provides a faster experience but may cause frequent content changes.
- **Manual**: Tabs must be explicitly activated using Enter or Space keys after focusing. This is recommended when tab panel content is computationally expensive or when rapid content changes could be disorienting.

## Props
Check out the full list of props on Skyscanner's [design system documentation website]( https://github.com/Skyscanner/backpack/blob/main/packages/bpk-component-segmented-control/README.md).

Check out the full list of props on Skyscanner's [design system documentation website]( https://www.skyscanner.design/latest/components/section-list/web-tP8t6vq8).
5 changes: 4 additions & 1 deletion packages/bpk-component-segmented-control/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
*/

import BpkSegmentedControl, {
useSegmentedControlPanels,
type Props as BpkSegmentControlProps,
type TabPanelProps,
} from './src/BpkSegmentedControl';

export type { BpkSegmentControlProps };
export type { BpkSegmentControlProps, TabPanelProps };
export { useSegmentedControlPanels };
export default BpkSegmentedControl;
Loading
Loading