-
Notifications
You must be signed in to change notification settings - Fork 0
102 create segmented control #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
scriptcoded
wants to merge
6
commits into
main
Choose a base branch
from
102-create-segmented-control
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+344
−4
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
30e9c12
feat: add segmented control
scriptcoded 56a73c0
docs: improve segmented control documentation
scriptcoded 76ff3c9
chore: add changeset
scriptcoded d780cfa
docs: use correct md heading
scriptcoded e9d18a4
fix: copilot suggestions
scriptcoded 53e79b3
fix: more copilot suggestions
scriptcoded File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@scouterna/ui-webc": minor | ||
| --- | ||
|
|
||
| Added Segmented Control component. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { | ||
| Meta, | ||
| Controls, | ||
| Description, | ||
| Primary, | ||
| Source, | ||
| Stories, | ||
| Subtitle, | ||
| Title, | ||
| } from '@storybook/addon-docs/blocks'; | ||
|
|
||
| import * as SegmentedControlStories from './segmented-control.stories'; | ||
|
|
||
| <Meta of={SegmentedControlStories} /> | ||
|
|
||
| <Title /> | ||
| <Subtitle /> | ||
| <Description /> | ||
| <Primary /> | ||
| <Controls /> | ||
|
|
||
| ## Code Examples | ||
|
|
||
| ### Controlling state | ||
|
|
||
| You must manage the state of the active segment yourself by listening to the | ||
| `scoutChange` event and updating the `value` prop accordingly. | ||
|
|
||
| <Source | ||
| language="tsx" | ||
| code={` | ||
| import { useState } from "react"; | ||
| import { ScoutSegmentedControl } from "@scouterna/ui-react"; | ||
|
|
||
| const [activeSegment, setActiveSegment] = useState(0); | ||
|
|
||
| return ( | ||
| <ScoutSegmentedControl | ||
| value={activeSegment} | ||
| onScoutChange={(e) => setActiveSegment(e.detail.value)} | ||
| > | ||
| <button type="button">Alla</button> | ||
| <button type="button">Bokade</button> | ||
| </ScoutSegmentedControl> | ||
| ); | ||
| `} | ||
| /> | ||
|
|
||
| ## Stories | ||
|
|
||
| <Stories title={false} /> |
36 changes: 36 additions & 0 deletions
36
packages/storybook/src/stories/segmented-control.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { ScoutSegmentedControl } from "@scouterna/ui-react"; | ||
| import { useArgs } from "storybook/internal/preview-api"; | ||
| import preview from "#.storybook/preview"; | ||
|
|
||
| const meta = preview.meta({ | ||
| title: "Interaction/Segmented Control", | ||
| component: ScoutSegmentedControl, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| }); | ||
|
|
||
| export default meta; | ||
|
|
||
| export const BasicExample = meta.story({ | ||
| args: {}, | ||
| render: (args) => { | ||
| const [_, setArgs] = useArgs(); | ||
|
|
||
| return ( | ||
| <ScoutSegmentedControl | ||
| {...args} | ||
| onScoutChange={(e) => setArgs({ value: e.detail.value })} | ||
| > | ||
| <button type="button">Alla</button> | ||
| <button type="button">Bokade</button> | ||
| </ScoutSegmentedControl> | ||
| ); | ||
| }, | ||
| }); | ||
|
|
||
| export const Small = BasicExample.extend({ | ||
| args: { | ||
| size: "small", | ||
| }, | ||
| }); |
34 changes: 34 additions & 0 deletions
34
packages/ui-webc/src/components/segmented-control/readme.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # scout-segmented-control | ||
|
|
||
| <!-- Auto Generated Below --> | ||
|
|
||
|
|
||
| ## Overview | ||
|
|
||
| The segmented control component presents a set of options where exactly one | ||
| option is active at a time. | ||
|
|
||
| The component displays an indicator under the selected option and emits a | ||
| `scoutChange` event when the user picks a different option, so you can update | ||
| `value`. | ||
|
|
||
| Use button elements as the slotted segment options. | ||
|
|
||
| ## Properties | ||
|
|
||
| | Property | Attribute | Description | Type | Default | | ||
| | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------- | | ||
| | `size` | `size` | Size of the input element. Large fields are typically used for prominent inputs, such as a top search field on a page, while medium fields are used for regular form inputs. | `"medium" \| "small"` | `"medium"` | | ||
| | `value` | `value` | Zero-based index of the currently active segment. | `number` | `0` | | ||
|
|
||
|
|
||
| ## Events | ||
|
|
||
| | Event | Description | Type | | ||
| | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | ||
| | `scoutChange` | Emitted when the active segment changes as a result of a user click. The `value` in the event detail is the zero-based index of the newly selected segment. | `CustomEvent<{ value: number; }>` | | ||
|
|
||
|
|
||
| ---------------------------------------------- | ||
|
|
||
| *Built with [StencilJS](https://stenciljs.com/)* |
67 changes: 67 additions & 0 deletions
67
packages/ui-webc/src/components/segmented-control/segmented-control.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| :host { | ||
| position: relative; | ||
| width: 100%; | ||
| display: flex; | ||
| height: var(--spacing-10); | ||
| border: 1px solid var(--color-gray-300); | ||
| border-radius: var(--spacing-2); | ||
| font: var(--type-body-md); | ||
| padding: 0 var(--indicator-padding); | ||
|
|
||
| --indicator-padding: 0.125rem; | ||
| --button-padding: var(--spacing-3); | ||
| } | ||
|
|
||
| :host(.small) { | ||
| height: var(--spacing-8); | ||
| font: var(--type-body-sm); | ||
|
|
||
| --button-padding: var(--spacing-2); | ||
| } | ||
|
|
||
| .indicator { | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 0; | ||
| height: 100%; | ||
| transition: all 0.2s ease; | ||
| pointer-events: none; | ||
| z-index: 0; | ||
|
|
||
scriptcoded marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| &::after { | ||
| content: ""; | ||
| position: absolute; | ||
| top: var(--indicator-padding); | ||
| left: 0; | ||
| right: 0; | ||
| bottom: var(--indicator-padding); | ||
| border-radius: 0.375rem; | ||
| background-color: var(--color-background-brand-base); | ||
| } | ||
| } | ||
|
|
||
| ::slotted(button) { | ||
| flex: 1; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0 var(--button-padding); | ||
| font-size: var(--font-size-2); | ||
| color: var(--color-text-default); | ||
| background-color: transparent; | ||
| border: none; | ||
| cursor: pointer; | ||
| transition: color 0.15s ease; | ||
| position: relative; | ||
| z-index: 1; | ||
|
|
||
| &:disabled { | ||
| color: var(--color-text-disabled); | ||
| cursor: not-allowed; | ||
| } | ||
| } | ||
|
|
||
| ::slotted(button[aria-checked="true"]) { | ||
| color: var(--color-text-brand-inverse); | ||
| } | ||
137 changes: 137 additions & 0 deletions
137
packages/ui-webc/src/components/segmented-control/segmented-control.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { | ||
| Component, | ||
| type ComponentInterface, | ||
| Element, | ||
| Event, | ||
| type EventEmitter, | ||
| Host, | ||
| h, | ||
| Listen, | ||
| Prop, | ||
| State, | ||
| Watch, | ||
| } from "@stencil/core"; | ||
|
|
||
| export type Size = "small" | "medium"; | ||
|
|
||
| /** | ||
| * The segmented control component presents a set of options where exactly one | ||
| * option is active at a time. | ||
| * | ||
| * The component displays an indicator under the selected option and emits a | ||
| * `scoutChange` event when the user picks a different option, so you can update | ||
| * `value`. | ||
| * | ||
| * Use button elements as the slotted segment options. | ||
| */ | ||
| @Component({ | ||
| tag: "scout-segmented-control", | ||
| styleUrl: "segmented-control.css", | ||
| shadow: { | ||
| delegatesFocus: true, | ||
| }, | ||
| }) | ||
scriptcoded marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| export class ScoutSegmentedControl implements ComponentInterface { | ||
| /** | ||
| * Size of the input element. Large fields are typically used for prominent | ||
| * inputs, such as a top search field on a page, while medium fields are used | ||
| * for regular form inputs. | ||
|
Comment on lines
+36
to
+38
|
||
| */ | ||
| @Prop() size: Size = "medium"; | ||
|
|
||
| /** | ||
| * Zero-based index of the currently active segment. | ||
| */ | ||
| @Prop() | ||
| public value: number = 0; | ||
|
|
||
| /** | ||
| * Emitted when the active segment changes as a result of a user click. | ||
| * The `value` in the event detail is the zero-based index of the newly selected segment. | ||
| */ | ||
| @Event() | ||
| public scoutChange!: EventEmitter<{ value: number }>; | ||
|
|
||
| @State() | ||
| private widths: number[] = []; | ||
| @State() | ||
| private lefts: number[] = []; | ||
|
|
||
| @State() | ||
| private enableAnimations = false; | ||
|
|
||
| @Element() el!: HTMLElement; | ||
|
|
||
| render() { | ||
| const sizeClass = this.size === "small" ? "small" : ""; | ||
| const noTransitionClass = this.enableAnimations ? "" : "no-transition"; | ||
|
|
||
| return ( | ||
| <Host class={`${sizeClass} ${noTransitionClass}`}> | ||
| <slot /> | ||
| {this.getIndicator()} | ||
| </Host> | ||
| ); | ||
|
Comment on lines
+65
to
+74
|
||
| } | ||
|
|
||
| componentDidLoad() { | ||
| this.updateChildrenAttributes(); | ||
| this.calculateIndicatorSizes(); | ||
|
|
||
| requestAnimationFrame(() => { | ||
| this.enableAnimations = true; | ||
| }); | ||
| } | ||
|
Comment on lines
+77
to
+84
|
||
|
|
||
| getIndicator() { | ||
| const width = this.widths[this.value] || 0; | ||
| const left = this.lefts[this.value] || 0; | ||
|
|
||
| const indicatorStyle = { | ||
| width: `${width}px`, | ||
| transform: `translateX(${left}px)`, | ||
| }; | ||
|
|
||
| return <div aria-hidden="true" class="indicator" style={indicatorStyle} />; | ||
| } | ||
|
|
||
| @Listen("click", { capture: true }) | ||
| handleClick(event: MouseEvent) { | ||
| const target = event.target as HTMLElement; | ||
| const buttons = Array.from(this.el.children); | ||
| const clickedIndex = buttons.indexOf(target); | ||
|
|
||
scriptcoded marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (clickedIndex !== -1 && clickedIndex !== this.value) { | ||
| this.scoutChange.emit({ value: clickedIndex }); | ||
| } | ||
| } | ||
|
|
||
| @Watch("value") | ||
| updateChildrenAttributes() { | ||
| Array.from(this.el.children).forEach((child, index) => { | ||
| const button = child as HTMLElement; | ||
| button.role = "radio"; | ||
| if (index === this.value) { | ||
| button.ariaChecked = "true"; | ||
| } else { | ||
| button.ariaChecked = "false"; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| @Watch("value") | ||
| calculateIndicatorSizes() { | ||
| // Get left padding of container | ||
| const baseLeft = parseFloat(getComputedStyle(this.el).paddingLeft) || 0; | ||
|
|
||
| this.widths = Array.from(this.el.children).map( | ||
| (child) => (child as HTMLElement).offsetWidth, | ||
| ); | ||
| this.lefts = this.widths.map( | ||
| (_, index) => | ||
| this.widths.slice(0, index).reduce((acc, w) => acc + w, 0) + baseLeft, | ||
| ); | ||
|
|
||
scriptcoded marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| console.log("Calculated widths:", this.widths); | ||
|
Comment on lines
+134
to
+135
|
||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.