diff --git a/.github/skills/code-review/CHECKLIST.md b/.github/skills/code-review/CHECKLIST.md new file mode 100644 index 00000000..9dc61263 --- /dev/null +++ b/.github/skills/code-review/CHECKLIST.md @@ -0,0 +1,73 @@ +# Catalyst Code Review Checklist + +Apply only sections relevant to changed paths. Mark every applicable rule ✅, ❌, or N/A with a reason. + +## Repository-wide + +- [ ] PR description explains user-facing or maintenance intent and scope. +- [ ] Commit messages follow Conventional Commits. If a scope is present, it is one of `core`, `angular`, `angular-formly`, `react`, `tokens`, or `release`; scope is review policy, not enforced as required by current commitlint config. +- [ ] Commit-message validation is absent from CI. Local commitlint runs from Husky's `commit-msg` hook and can be bypassed; inspect commits when relevant. +- [ ] No secrets, credentials, `.env` files, or sensitive infrastructure information are added. +- [ ] Dependency, lockfile, CI, release, deployment, or infrastructure changes are intentional, minimal, and documented. Escalate shared-infrastructure changes for platform review. +- [ ] No generated output is hand-edited: `dist/`, `core/loader/`, `core/www/`, `.stencil/`, generated Angular proxies, or generated React bindings. + +## Tokens (`tokens/**`) + +- [ ] Token source follows existing design-token structure. +- [ ] Token build output is regenerated and reviewed; affected Core consumers are rebuilt when token names or values change. +- [ ] Token changes preserve consumer compatibility or include a deprecation/migration path. + +## Core public contract (`core/src/components/**`) + +- [ ] Public props, events, methods, slots, CSS parts, and form behavior remain backward compatible; removals or semantic changes include deprecation and migration guidance. +- [ ] Public API changes regenerate and review `angular/projects/catalyst/src/lib/directives/proxies.ts` and `react/src/components/stencil-generated/index.ts`; Angular and React builds pass. +- [ ] Changes to `cat-input`, `cat-textarea`, `cat-datepicker`, `cat-datepicker-inline`, `cat-checkbox`, `cat-toggle`, `cat-radio`, `cat-radio-group`, `cat-select`, or `cat-tag` preserve or deliberately update value-accessor bindings in `core/stencil.config.ts`. + +### Props, events, and methods + +- [ ] Boolean props use explicit `false` defaults when false is their default behavior. +- [ ] Multi-word props are camelCase; string unions reuse established scales unless an intentional contract addition is documented. +- [ ] Public `@Prop()`, `@Event()`, and `@Method()` members have accurate JSDoc. +- [ ] Deprecated APIs remain available and their JSDoc gives actionable replacement or migration guidance. +- [ ] Events use `cat` prefix and precise `EventEmitter` payloads; value-change events emit their value type. +- [ ] Native-element wrappers expose `doFocus`, `doBlur`, or `doClick` as appropriate; documentation directs consumers away from inner-element access. +- [ ] Public `@Method()` members are asynchronous and return `Promise` unless an established contract requires another return type. + +### Native controls and parts + +- [ ] Components wrapping a native control expose `@Element()`, `testId`, and `nativeAttributes` when an inner native element can receive them; `testId` maps to `data-test` and `nativeAttributes` is forwarded. +- [ ] Component JSDoc declares every exposed CSS part; every declared part appears in render output. +- [ ] Parts expose consumer-meaningful surfaces, not internal layout or state-only wrappers. + +### SCSS and Shadow DOM + +- [ ] Component SCSS uses established imports, token helpers, mixins, `:host` layout, and `:host([hidden]) { display: none; }` where applicable. +- [ ] Colors, typography, radii, and transitions use existing token or mixin patterns. Hardcoded color values are rejected; token-backed CSS custom-property and RGB consumption is allowed. +- [ ] Vendor-prefix exceptions use narrow paired Stylelint disable/enable comments. +- [ ] Shadow-boundary ancestor traversal uses `findClosest`; outside-click detection uses `event.composedPath()`. +- [ ] Focusable wrappers use `shadow: { delegatesFocus: true }` when this matches established component behavior. + +### Accessibility and tests + +- [ ] Keyboard behavior, focus movement/restoration, disabled behavior, labels, semantic roles, and ARIA state remain correct. +- [ ] Render-affecting prop changes have spec coverage. +- [ ] Interaction, focus, value, or event changes have e2e coverage. +- [ ] Visual variants or state changes have screenshot coverage; baseline changes are intentional and reviewed. + +## Angular and Formly (`angular/**`) + +- [ ] Public Angular or Formly API changes preserve compatibility or include deprecation and migration guidance. +- [ ] Changes are covered by relevant Angular or Formly tests and `build:angular` / `build:angular-formly` validation. +- [ ] Generated Core proxies are not hand-edited. + +## React (`react/**`) + +- [ ] Public wrapper API changes preserve compatibility or include deprecation and migration guidance. +- [ ] Generated Stencil bindings are not hand-edited. +- [ ] React build passes; add targeted tests when test infrastructure exists or behavior changes warrant coverage. + +## Tooling and release + +- [ ] CI changes map correctly to affected paths and preserve required build order: tokens → core → Angular/Formly and tokens → core → React. +- [ ] Public breaking changes are explicitly flagged with consumer migration path; linked release versions do not eliminate compatibility review. +- [ ] Formatter, linter, builds, and relevant tests were run or CI status proves their applicable execution. diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 00000000..d813d1de --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,46 @@ +--- +name: code-review +description: Review Catalyst pull requests. Use for pull request and code review, especially changes to Catalyst components, tokens, Angular, Angular Formly, React bindings, generated bindings, CI checks, or public API contracts. +--- + +# Catalyst Code Review + +The **gate**: approve only when every applicable check passes or is explicitly N/A. Report all blocking findings in one review. + +## Step 1 — Establish review context + +Use current pull request context. If unavailable, ask for its URL or number. Inspect the description, base and head commits, changed files, commits, existing review threads, and check runs. + +Map changed paths to applicable workflows: + +| Changed path | Workflow | Checks | +|--------------|----------|-------------------| +| `tokens/**` | Tokens | build | +| `core/**` | Core | build, lint, test | +| `angular/**` | Angular | build, test | +| `react/**` | React | build | + +Workflow paths are authoritative; check `.github/workflows/` when they change. React and Tokens have no dedicated lint or test workflow. If a public Core contract changes, also require generated binding review and Angular and React consumer builds: their workflows do not run for `core/**` alone. + +Record failed applicable checks. Continue static review, but do not approve while required applicable checks fail. + +Completion criterion: changed areas and applicable checks identified; every failed check recorded. + +## Step 2 — Apply applicable checks + +Load [CHECKLIST.md](CHECKLIST.md). Select sections by changed area. Mark every applicable item ✅, ❌, or N/A with a short reason. For every ❌, record exact file, line, evidence, and required correction. + +Inspect changed source, tests, generated bindings, public contracts, and dependency or tooling changes; do not infer correctness from green CI. + +Completion criterion: every applicable checklist item accounted for; every blocking finding has actionable evidence. + +## Step 3 — Publish one review + +- Lead with one sentence describing PR effect. +- List ❌ items as required changes with file, line, evidence, and rule. +- Label non-blocking observations as suggestions. +- Request changes for any blocking finding. +- Approve only when no blocking findings exist and all required applicable checks pass. +- Otherwise leave a comment review describing pending failed or missing checks. + +Completion criterion: one complete review published or drafted, with no unreported blocking finding. diff --git a/.gitignore b/.gitignore index 7a1537ba..b71d442f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea node_modules + +# e2e screenshots are for error reporting only and not part of the source code +**/__screenshots__/**/*.e2e.tsx/** diff --git a/README.md b/README.md index 8b5a9447..3f5ea715 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,17 @@ Steps: 1. Make changes; 2. Run `pnpm build`; -Extra steps for testing the changes in angular package: +`pnpm run build:core` regenerates Angular and React binding files through +Stencil output targets. Review and commit generated changes with components: + +- `angular/projects/catalyst/src/lib/directives/proxies.ts` +- `react/src/components/stencil-generated/index.ts` + +`pnpm run build:angular` and `pnpm run build:react` validate and compile those +bindings. CI does not commit generated binding changes. Do not edit generated +files manually. + +Extra steps for testing the changes in Angular package: 1. Run `pnpm install` again; 2. For - [angular](angular/projects/catalyst) Go to `/angular/dist/catalyst`; @@ -117,13 +127,13 @@ Both local and CI reference screenshots are committed to the repository so that Screenshot tests are intentionally excluded from `pnpm run test`. To run them locally: ``` -pnpm run test:screenshot # from core/ +pnpm run test:screenshot # from repo root ``` To update the local (macOS) reference screenshots after an intentional visual change: ``` -pnpm run test:screenshot:update # from core/ +pnpm run test:screenshot:update # from repo root ``` This regenerates the `-darwin` screenshots and commits them alongside your changes. diff --git a/angular/projects/catalyst/src/lib/catalyst.module.ts b/angular/projects/catalyst/src/lib/catalyst.module.ts index 771613ac..5c518edc 100644 --- a/angular/projects/catalyst/src/lib/catalyst.module.ts +++ b/angular/projects/catalyst/src/lib/catalyst.module.ts @@ -36,6 +36,8 @@ const CatComponents = [ Components.CatRadio, Components.CatRadioGroup, Components.CatScrollable, + Components.CatSegment, + Components.CatSegmentedControl, Components.CatSelect, Components.CatSkeleton, Components.CatSpinner, diff --git a/angular/projects/catalyst/src/lib/directives/proxies.ts b/angular/projects/catalyst/src/lib/directives/proxies.ts index 9b6c9364..33bd5ff0 100644 --- a/angular/projects/catalyst/src/lib/directives/proxies.ts +++ b/angular/projects/catalyst/src/lib/directives/proxies.ts @@ -1193,6 +1193,82 @@ export declare interface CatScrollable extends Components.CatScrollable { scrolledBottom: EventEmitter>; } +@ProxyCmp({ + inputs: ['a11yLabel', 'disabled', 'icon', 'iconOnly', 'nativeAttributes', 'testId', 'value'], + methods: ['doFocus', 'doBlur'] +}) +@Component({ + selector: 'cat-segment', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: [ + 'a11yLabel', + 'disabled', + 'icon', + 'iconOnly', + 'nativeAttributes', + 'testId', + { name: 'value', required: true } + ], + standalone: false +}) +export class CatSegment { + protected el: HTMLCatSegmentElement; + constructor( + c: ChangeDetectorRef, + r: ElementRef, + protected z: NgZone + ) { + c.detach(); + this.el = r.nativeElement; + } +} + +export declare interface CatSegment extends Components.CatSegment {} + +@ProxyCmp({ + inputs: ['a11yLabel', 'disabled', 'nativeAttributes', 'size', 'testId', 'value'] +}) +@Component({ + selector: 'cat-segmented-control', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['a11yLabel', 'disabled', 'nativeAttributes', 'size', 'testId', 'value'], + outputs: ['catChange', 'catFocus', 'catBlur'], + standalone: false +}) +export class CatSegmentedControl { + protected el: HTMLCatSegmentedControlElement; + @Output() catChange = new EventEmitter>(); + @Output() catFocus = new EventEmitter>(); + @Output() catBlur = new EventEmitter>(); + constructor( + c: ChangeDetectorRef, + r: ElementRef, + protected z: NgZone + ) { + c.detach(); + this.el = r.nativeElement; + } +} + +export declare interface CatSegmentedControl extends Components.CatSegmentedControl { + /** + * Emitted when the selected segment changes. Payload is the new segment value. + */ + catChange: EventEmitter>; + /** + * Emitted when a segment gains focus. Payload is the segment value. + */ + catFocus: EventEmitter>; + /** + * Emitted when a segment loses focus. Payload is the segment value. + */ + catBlur: EventEmitter>; +} + @ProxyCmp({ inputs: [ 'autoComplete', diff --git a/core/.gitignore b/core/.gitignore index 5cd374b7..6e75b111 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -2,6 +2,7 @@ dist/ www/ loader/ coverage/ +.vitest-attachments/ *~ *.sw[mnpcod] diff --git a/core/package.json b/core/package.json index 30507f8e..aa99c721 100644 --- a/core/package.json +++ b/core/package.json @@ -33,6 +33,8 @@ "test": "stencil-test --project plugin", "test:screenshot": "stencil-test --project browser", "test:e2e": "stencil-test --project e2e", + "posttest": "node replace && pnpm --filter @haiilo/catalyst-angular-workspace run prettier", + "posttest:screenshot": "node replace && pnpm --filter @haiilo/catalyst-angular-workspace run prettier", "posttest:e2e": "node replace && pnpm --filter @haiilo/catalyst-angular-workspace run prettier", "test:watch": "stencil-test watch", "generate": "stencil generate", diff --git a/core/src/components.d.ts b/core/src/components.d.ts index c7c49227..4f498ea9 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -12,6 +12,7 @@ import { CatDatepickerMode } from "./components/cat-datepicker/cat-datepicker.mo import { BaseOptions } from "flatpickr/dist/types/options"; import { InputType } from "./components/cat-input/input-type"; import { FormatDateMaskOptions, FormatTimeMaskOptions } from "./components/cat-input/cat-input"; +import { SegmentSize } from "./components/cat-segment/cat-segment"; import { CatSelectConnector, CatSelectValue, Item } from "./components/cat-select/cat-select"; import { Observable } from "rxjs"; import { TooltipPlacement } from "./components/cat-tooltip/cat-tooltip"; @@ -22,6 +23,7 @@ export { CatDatepickerMode } from "./components/cat-datepicker/cat-datepicker.mo export { BaseOptions } from "flatpickr/dist/types/options"; export { InputType } from "./components/cat-input/input-type"; export { FormatDateMaskOptions, FormatTimeMaskOptions } from "./components/cat-input/cat-input"; +export { SegmentSize } from "./components/cat-segment/cat-segment"; export { CatSelectConnector, CatSelectValue, Item } from "./components/cat-select/cat-select"; export { Observable } from "rxjs"; export { TooltipPlacement } from "./components/cat-tooltip/cat-tooltip"; @@ -1435,6 +1437,92 @@ export namespace Components { */ "scrolledBuffer": number; } + /** + * A single segment within a cat-segmented-control. + */ + interface CatSegment { + /** + * Adds accessible label for the button that is only shown for screen readers. Typically, this label text replaces the visible text on the button for users who use assistive technology. + */ + "a11yLabel"?: string; + /** + * @default false + */ + "active": boolean; + /** + * Whether this segment is disabled. + * @default false + */ + "disabled": boolean; + /** + * Removes focus from the segment button. + */ + "doBlur": () => Promise; + /** + * Sets focus on the segment button. + */ + "doFocus": () => Promise; + /** + * Icon name to display (same registry as cat-button). + */ + "icon"?: string; + /** + * When true, hides the label and shows the icon only. Requires icon and a11y-label. + * @default false + */ + "iconOnly": boolean; + /** + * Spread onto the native button element. + */ + "nativeAttributes"?: { [key: string]: string }; + /** + * @default 0 + */ + "rovingTabIndex": number; + /** + * @default 'm' + */ + "size": SegmentSize; + /** + * Renders as data-test on the button element. + */ + "testId"?: string; + /** + * The unique value of this segment within the group. + */ + "value": string; + } + /** + * A segmented control for selecting one option from a mutually-exclusive set. + */ + interface CatSegmentedControl { + /** + * Accessible label for the group (screen readers only). + */ + "a11yLabel"?: string; + /** + * Whether the entire control is disabled. + * @default false + */ + "disabled": boolean; + /** + * Spread onto the root element. + */ + "nativeAttributes"?: { [key: string]: string }; + /** + * The size of the control, propagated to all child segments. + * @default 'm' + */ + "size": 'xs' | 's' | 'm' | 'l' | 'xl'; + /** + * Renders as data-test on the root element. + */ + "testId"?: string; + /** + * The value of the currently selected segment. + */ + "value"?: string; + } /** * Select lets user choose one option from an options' menu. * Consider using select when you have 6 or more options. Select component supports any content type. @@ -2246,6 +2334,14 @@ export interface CatScrollableCustomEvent extends CustomEvent { detail: T; target: HTMLCatScrollableElement; } +export interface CatSegmentCustomEvent extends CustomEvent { + detail: T; + target: HTMLCatSegmentElement; +} +export interface CatSegmentedControlCustomEvent extends CustomEvent { + detail: T; + target: HTMLCatSegmentedControlElement; +} export interface CatSelectCustomEvent extends CustomEvent { detail: T; target: HTMLCatSelectElement; @@ -2662,6 +2758,50 @@ declare global { prototype: HTMLCatScrollableElement; new (): HTMLCatScrollableElement; }; + interface HTMLCatSegmentElementEventMap { + "catSegmentClick": string; + "catSegmentFocus": string; + "catSegmentBlur": string; + } + /** + * A single segment within a cat-segmented-control. + */ + interface HTMLCatSegmentElement extends Components.CatSegment, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLCatSegmentElement, ev: CatSegmentCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLCatSegmentElement, ev: CatSegmentCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLCatSegmentElement: { + prototype: HTMLCatSegmentElement; + new (): HTMLCatSegmentElement; + }; + interface HTMLCatSegmentedControlElementEventMap { + "catChange": string; + "catFocus": string; + "catBlur": string; + } + /** + * A segmented control for selecting one option from a mutually-exclusive set. + */ + interface HTMLCatSegmentedControlElement extends Components.CatSegmentedControl, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLCatSegmentedControlElement, ev: CatSegmentedControlCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLCatSegmentedControlElement, ev: CatSegmentedControlCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLCatSegmentedControlElement: { + prototype: HTMLCatSegmentedControlElement; + new (): HTMLCatSegmentedControlElement; + }; interface HTMLCatSelectElementEventMap { "catOpen": FocusEvent; "catClose": FocusEvent; @@ -2875,6 +3015,8 @@ declare global { "cat-radio": HTMLCatRadioElement; "cat-radio-group": HTMLCatRadioGroupElement; "cat-scrollable": HTMLCatScrollableElement; + "cat-segment": HTMLCatSegmentElement; + "cat-segmented-control": HTMLCatSegmentedControlElement; "cat-select": HTMLCatSelectElement; "cat-select-demo": HTMLCatSelectDemoElement; "cat-skeleton": HTMLCatSkeletonElement; @@ -2889,6 +3031,8 @@ declare global { } } declare namespace LocalJSX { + type OneOf = { [P in K]: PropT } & { [P in `attr:${K}` | `prop:${K}`]?: never } | { [P in `attr:${K}`]: AttrT } & { [P in K | `prop:${K}`]?: never } | { [P in `prop:${K}`]: PropT } & { [P in K | `attr:${K}`]?: never }; + /** * Informs user about important changes or conditions in the interface. Use this * component if you need to capture user’s attention in a prominent way. @@ -4333,6 +4477,99 @@ declare namespace LocalJSX { */ "scrolledBuffer"?: number; } + /** + * A single segment within a cat-segmented-control. + */ + interface CatSegment { + /** + * Adds accessible label for the button that is only shown for screen readers. Typically, this label text replaces the visible text on the button for users who use assistive technology. + */ + "a11yLabel"?: string; + /** + * @default false + */ + "active"?: boolean; + /** + * Whether this segment is disabled. + * @default false + */ + "disabled"?: boolean; + /** + * Icon name to display (same registry as cat-button). + */ + "icon"?: string; + /** + * When true, hides the label and shows the icon only. Requires icon and a11y-label. + * @default false + */ + "iconOnly"?: boolean; + /** + * Spread onto the native button element. + */ + "nativeAttributes"?: { [key: string]: string }; + "onCatSegmentBlur"?: (event: CatSegmentCustomEvent) => void; + "onCatSegmentClick"?: (event: CatSegmentCustomEvent) => void; + "onCatSegmentFocus"?: (event: CatSegmentCustomEvent) => void; + /** + * @default 0 + */ + "rovingTabIndex"?: number; + /** + * @default 'm' + */ + "size"?: SegmentSize; + /** + * Renders as data-test on the button element. + */ + "testId"?: string; + /** + * The unique value of this segment within the group. + */ + "value": string; + } + /** + * A segmented control for selecting one option from a mutually-exclusive set. + */ + interface CatSegmentedControl { + /** + * Accessible label for the group (screen readers only). + */ + "a11yLabel"?: string; + /** + * Whether the entire control is disabled. + * @default false + */ + "disabled"?: boolean; + /** + * Spread onto the root element. + */ + "nativeAttributes"?: { [key: string]: string }; + /** + * Emitted when a segment loses focus. Payload is the segment value. + */ + "onCatBlur"?: (event: CatSegmentedControlCustomEvent) => void; + /** + * Emitted when the selected segment changes. Payload is the new segment value. + */ + "onCatChange"?: (event: CatSegmentedControlCustomEvent) => void; + /** + * Emitted when a segment gains focus. Payload is the segment value. + */ + "onCatFocus"?: (event: CatSegmentedControlCustomEvent) => void; + /** + * The size of the control, propagated to all child segments. + * @default 'm' + */ + "size"?: 'xs' | 's' | 'm' | 'l' | 'xl'; + /** + * Renders as data-test on the root element. + */ + "testId"?: string; + /** + * The value of the currently selected segment. + */ + "value"?: string; + } /** * Select lets user choose one option from an options' menu. * Consider using select when you have 6 or more options. Select component supports any content type. @@ -5377,6 +5614,24 @@ declare namespace LocalJSX { "noScrolledInit": boolean; "scrolledBuffer": number; } + interface CatSegmentAttributes { + "active": boolean; + "disabled": boolean; + "icon": string; + "iconOnly": boolean; + "a11yLabel": string; + "size": SegmentSize; + "rovingTabIndex": number; + "testId": string; + "value": string; + } + interface CatSegmentedControlAttributes { + "value": string; + "size": 'xs' | 's' | 'm' | 'l' | 'xl'; + "disabled": boolean; + "a11yLabel": string; + "testId": string; + } interface CatSelectAttributes { "requiredMarker": 'none' | 'required' | 'optional' | 'none!' | 'optional!' | 'required!'; "horizontal": boolean; @@ -5548,6 +5803,8 @@ declare namespace LocalJSX { "cat-radio": Omit & { [K in keyof CatRadio & keyof CatRadioAttributes]?: CatRadio[K] } & { [K in keyof CatRadio & keyof CatRadioAttributes as `attr:${K}`]?: CatRadioAttributes[K] } & { [K in keyof CatRadio & keyof CatRadioAttributes as `prop:${K}`]?: CatRadio[K] }; "cat-radio-group": Omit & { [K in keyof CatRadioGroup & keyof CatRadioGroupAttributes]?: CatRadioGroup[K] } & { [K in keyof CatRadioGroup & keyof CatRadioGroupAttributes as `attr:${K}`]?: CatRadioGroupAttributes[K] } & { [K in keyof CatRadioGroup & keyof CatRadioGroupAttributes as `prop:${K}`]?: CatRadioGroup[K] }; "cat-scrollable": Omit & { [K in keyof CatScrollable & keyof CatScrollableAttributes]?: CatScrollable[K] } & { [K in keyof CatScrollable & keyof CatScrollableAttributes as `attr:${K}`]?: CatScrollableAttributes[K] } & { [K in keyof CatScrollable & keyof CatScrollableAttributes as `prop:${K}`]?: CatScrollable[K] }; + "cat-segment": Omit & { [K in keyof CatSegment & keyof CatSegmentAttributes]?: CatSegment[K] } & { [K in keyof CatSegment & keyof CatSegmentAttributes as `attr:${K}`]?: CatSegmentAttributes[K] } & { [K in keyof CatSegment & keyof CatSegmentAttributes as `prop:${K}`]?: CatSegment[K] } & OneOf<"value", CatSegment["value"], CatSegmentAttributes["value"]>; + "cat-segmented-control": Omit & { [K in keyof CatSegmentedControl & keyof CatSegmentedControlAttributes]?: CatSegmentedControl[K] } & { [K in keyof CatSegmentedControl & keyof CatSegmentedControlAttributes as `attr:${K}`]?: CatSegmentedControlAttributes[K] } & { [K in keyof CatSegmentedControl & keyof CatSegmentedControlAttributes as `prop:${K}`]?: CatSegmentedControl[K] }; "cat-select": Omit & { [K in keyof CatSelect & keyof CatSelectAttributes]?: CatSelect[K] } & { [K in keyof CatSelect & keyof CatSelectAttributes as `attr:${K}`]?: CatSelectAttributes[K] } & { [K in keyof CatSelect & keyof CatSelectAttributes as `prop:${K}`]?: CatSelect[K] }; "cat-select-demo": CatSelectDemo; "cat-skeleton": Omit & { [K in keyof CatSkeleton & keyof CatSkeletonAttributes]?: CatSkeleton[K] } & { [K in keyof CatSkeleton & keyof CatSkeletonAttributes as `attr:${K}`]?: CatSkeletonAttributes[K] } & { [K in keyof CatSkeleton & keyof CatSkeletonAttributes as `prop:${K}`]?: CatSkeleton[K] }; @@ -5661,6 +5918,14 @@ declare module "@stencil/core" { * An element to display scrollable content. */ "cat-scrollable": LocalJSX.IntrinsicElements["cat-scrollable"] & JSXBase.HTMLAttributes; + /** + * A single segment within a cat-segmented-control. + */ + "cat-segment": LocalJSX.IntrinsicElements["cat-segment"] & JSXBase.HTMLAttributes; + /** + * A segmented control for selecting one option from a mutually-exclusive set. + */ + "cat-segmented-control": LocalJSX.IntrinsicElements["cat-segmented-control"] & JSXBase.HTMLAttributes; /** * Select lets user choose one option from an options' menu. * Consider using select when you have 6 or more options. Select component supports any content type. diff --git a/core/src/components/cat-icon/readme.md b/core/src/components/cat-icon/readme.md index 27d474e9..ce20c84c 100644 --- a/core/src/components/cat-icon/readme.md +++ b/core/src/components/cat-icon/readme.md @@ -41,6 +41,7 @@ doesn't fit. - [cat-badge](../cat-badge) - [cat-button](../cat-button) - [cat-input](../cat-input) + - [cat-segment](../cat-segment) - [cat-select](../cat-select) - [cat-tag](../cat-tag) - [cat-textarea](../cat-textarea) @@ -53,6 +54,7 @@ graph TD; cat-badge --> cat-icon cat-button --> cat-icon cat-input --> cat-icon + cat-segment --> cat-icon cat-select --> cat-icon cat-tag --> cat-icon cat-textarea --> cat-icon diff --git a/core/src/components/cat-segment/cat-segment.e2e.tsx b/core/src/components/cat-segment/cat-segment.e2e.tsx new file mode 100644 index 00000000..665915e8 --- /dev/null +++ b/core/src/components/cat-segment/cat-segment.e2e.tsx @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; +import { render, h } from '@stencil/vitest'; + +describe('cat-segment', () => { + it('renders hydrated', async () => { + const { root } = await render(Day); + await expect.element(root).toHaveClass('hydrated'); + }); + + it('delegates focus to its native button', async () => { + const { root } = await render(Day); + const button = root.shadowRoot?.querySelector('button'); + + button?.focus(); + + expect(document.activeElement).toBe(root); + expect(root.shadowRoot?.activeElement).toBe(button); + }); + + it('focuses and blurs through public methods', async () => { + const { root } = await render(Day); + const button = root.shadowRoot?.querySelector('button'); + + await root.doFocus(); + expect(root.shadowRoot?.activeElement).toBe(button); + + await root.doBlur(); + expect(root.shadowRoot?.activeElement).toBeNull(); + }); + + it('emits catSegmentClick on click', async () => { + const { root } = await render(Day); + const events: CustomEvent[] = []; + root.addEventListener('catSegmentClick', (e: Event) => events.push(e as CustomEvent)); + root.shadowRoot?.querySelector('button')?.click(); + expect(events).toHaveLength(1); + expect(events[0].detail).toBe('day'); + expect(events[0].bubbles).toBe(true); + expect(events[0].composed).toBe(true); + }); + + it('does not emit catSegmentClick when disabled', async () => { + const { root } = await render( + + Day + + ); + const events: Event[] = []; + root.addEventListener('catSegmentClick', e => events.push(e)); + root.shadowRoot?.querySelector('button')?.click(); + expect(events).toHaveLength(0); + }); +}); diff --git a/core/src/components/cat-segment/cat-segment.scss b/core/src/components/cat-segment/cat-segment.scss new file mode 100644 index 00000000..9e552ee3 --- /dev/null +++ b/core/src/components/cat-segment/cat-segment.scss @@ -0,0 +1,93 @@ +@use 'variables' as *; +@use 'mixins' as *; +@use 'sass:map'; + +$segment-heights: ( + 'xl': 3.25rem, + 'l': 2.75rem, + 'm': 2rem, + 's': 1.75rem, + 'xs': 1.5rem +); + +:host { + display: contents; +} + +:host([hidden]) { + display: none; +} + +.cat-segment { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + border: none; + background: transparent; + cursor: pointer; + font-family: inherit; + white-space: nowrap; + color: cat-token('color.theme.secondary.text'); + border-radius: cat-border-radius('l'); + transition: + background cat-token('time.transition.s') linear, + box-shadow cat-token('time.transition.s') linear, + color cat-token('time.transition.s') linear; + + &.cat-segment-active { + background: cat-token('color.ui.background.surface'); + color: cat-token('color.theme.primary.text'); + font-weight: 700; + @include cat-elevation(3); + } + + &:focus-visible { + outline: 2px solid cat-token('color.ui.border.focus'); + outline-offset: -2px; + } + + &:disabled { + color: cat-token('color.ui.font.muted', 0.5); + cursor: not-allowed; + pointer-events: none; + } + + &:not(:disabled) { + &:hover { + background: cat-token('color.ui.background.surface', 0.5); + } + } +} + +.cat-segment-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin size($size, $fontSize, $padding, $gap: 0.25rem) { + $-line-height: cat-token('size.line.body.#{$fontSize}'); + + $-total-height: map.get($segment-heights, $size); + $-padding-v: ($-total-height - $-line-height) * 0.5; + $-padding-h: $padding; + + .cat-segment-#{$size} { + width: 12.5em; + padding: $-padding-v $-padding-h; + @include cat-body($fontSize, null); + gap: $gap; + + &.cat-segment-icon-only { + height: $-total-height; + } + } +} + +@include size('xs', 's', 0.25rem); +@include size('s', 'm', 0.5rem); +@include size('m', 'm', 0.75rem); +@include size('l', 'm', 1rem, 0.5rem); +@include size('xl', 'l', 1.25rem, 0.75rem); diff --git a/core/src/components/cat-segment/cat-segment.spec.tsx b/core/src/components/cat-segment/cat-segment.spec.tsx new file mode 100644 index 00000000..95a06691 --- /dev/null +++ b/core/src/components/cat-segment/cat-segment.spec.tsx @@ -0,0 +1,194 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render } from '@stencil/vitest'; +import { h } from '@stencil/core'; +import './cat-segment'; + +describe('cat-segment', () => { + it('renders', async () => { + const { root } = await render(Label); + + expect(root.innerHTML).toBe('Label'); + expect(root.value).toBe('testValue'); + }); + + it('renders button in shadow root', async () => { + const { root } = await render(Day); + const btn = root.shadowRoot?.querySelector('button'); + + expect(btn).toHaveClass('cat-segment-m'); + expect(btn).not.toHaveClass('cat-segment-active'); + expect(btn?.getAttribute('aria-checked')).toBe('false'); + }); + + it('applies size class', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + expect(btn).toHaveClass('cat-segment-xl'); + }); + + it('applies active class when active', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + expect(btn).toHaveClass('cat-segment-active'); + expect(btn?.getAttribute('aria-checked')).toBe('true'); + }); + + it('renders disabled attribute when disabled', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + expect(btn).toHaveAttribute('disabled'); + }); + + it('renders an icon and hides its label in icon-only mode', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + + expect(root.shadowRoot?.querySelector('cat-icon')).toBeTruthy(); + expect(root.shadowRoot?.querySelector('.cat-segment-label')).toBeNull(); + expect(btn).toHaveClass('cat-segment-icon-only'); + }); + + it('keeps the label when icon-only is set without an icon', async () => { + const { root } = await render( + + Day + + ); + + expect(root.shadowRoot?.querySelector('.cat-segment-label')).toBeTruthy(); + expect(root.shadowRoot?.querySelector('button')).not.toHaveClass('cat-segment-icon-only'); + }); + + it.each([ + ['xs', 's'], + ['s', 's'], + ['m', 's'], + ['l', 'm'], + ['xl', 'l'] + ] as const)('maps %s segment size to %s icon size', async (size, iconSize) => { + const { root } = await render( + + Day + + ); + + expect(root.shadowRoot?.querySelector('cat-icon')?.getAttribute('size')).toBe(iconSize); + }); + + it('applies test id and preserves required button semantics over native attributes', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + + expect(btn?.getAttribute('data-test')).toBe('segment'); + expect(btn?.getAttribute('role')).toBe('radio'); + expect(btn?.getAttribute('type')).toBe('button'); + expect(btn?.getAttribute('aria-checked')).toBe('false'); + expect(btn?.getAttribute('tabindex')).toBe('0'); + expect(btn?.getAttribute('name')).toBe('day'); + }); + + it('emits its value when an inactive segment is clicked', async () => { + const { root } = await render(Day); + const listener = vi.fn(); + root.addEventListener('catSegmentClick', listener); + + root.shadowRoot?.querySelector('button')?.click(); + + expect(listener).toHaveBeenCalledOnce(); + expect(listener.mock.calls[0][0]).toMatchObject({ detail: 'day' }); + }); + + it.each([ + ['active', { active: true }], + ['disabled', { disabled: true }] + ] as const)('does not emit when clicked while %s', async (_state, props) => { + const { root } = await render( + + Day + + ); + const listener = vi.fn(); + root.addEventListener('catSegmentClick', listener); + + root.shadowRoot?.querySelector('button')?.click(); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('updates button state when mutable props change', async () => { + const { root, waitForChanges } = await render(Day); + + root.active = true; + root.size = 'xl'; + root.disabled = true; + await waitForChanges(); + + const btn = root.shadowRoot?.querySelector('button'); + expect(btn).toHaveClasses(['cat-segment-active', 'cat-segment-xl']); + expect(btn?.getAttribute('aria-checked')).toBe('true'); + expect(btn).toHaveAttribute('disabled'); + }); + + it('focuses and blurs its native button through public methods', async () => { + const { root } = await render(Day); + const btn = root.shadowRoot?.querySelector('button'); + const focus = vi.spyOn(btn!, 'focus'); + const blur = vi.spyOn(btn!, 'blur'); + + await root.doFocus(); + expect(focus).toHaveBeenCalledOnce(); + + await root.doBlur(); + expect(blur).toHaveBeenCalledOnce(); + }); + + it('emits focus and blur events with its value', async () => { + const { root } = await render(Day); + const focusListener = vi.fn(); + const blurListener = vi.fn(); + root.addEventListener('catSegmentFocus', focusListener); + root.addEventListener('catSegmentBlur', blurListener); + + root.shadowRoot?.querySelector('button')?.focus(); + expect(focusListener).toHaveBeenCalledOnce(); + expect(focusListener.mock.calls[0][0]).toMatchObject({ detail: 'day' }); + + root.shadowRoot?.querySelector('button')?.blur(); + expect(blurListener).toHaveBeenCalledOnce(); + expect(blurListener.mock.calls[0][0]).toMatchObject({ detail: 'day' }); + }); + + it('sets aria-label', async () => { + const { root } = await render( + + Day + + ); + const btn = root.shadowRoot?.querySelector('button'); + expect(btn?.getAttribute('aria-label')).toBe('Test aria label'); + }); +}); diff --git a/core/src/components/cat-segment/cat-segment.tsx b/core/src/components/cat-segment/cat-segment.tsx new file mode 100644 index 00000000..d2c663da --- /dev/null +++ b/core/src/components/cat-segment/cat-segment.tsx @@ -0,0 +1,135 @@ +import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core'; + +export type SegmentSize = 'xs' | 's' | 'm' | 'l' | 'xl'; + +/** + * A single segment within a cat-segmented-control. + * + * @part button - The native button element. + */ +@Component({ + tag: 'cat-segment', + styleUrl: 'cat-segment.scss', + shadow: { delegatesFocus: true } +}) +export class CatSegment { + private button?: HTMLButtonElement; + + @Element() hostElement!: HTMLElement; + + /** @internal Set by cat-segmented-control. Whether this segment is the active selection. */ + @Prop({ mutable: true }) active = false; + + /** Whether this segment is disabled. */ + @Prop({ mutable: true }) disabled = false; + + /** Icon name to display (same registry as cat-button). */ + @Prop() icon?: string; + + /** When true, hides the label and shows the icon only. Requires icon and a11y-label. */ + @Prop() iconOnly = false; + + /** + * Adds accessible label for the button that is only shown for screen + * readers. Typically, this label text replaces the visible text on the + * button for users who use assistive technology. + */ + @Prop({ attribute: 'a11y-label' }) a11yLabel?: string; + + /** @internal Set by cat-segmented-control. Size of the segment. */ + @Prop({ mutable: true }) size: SegmentSize = 'm'; + + /** @internal Set by cat-segmented-control for radio roving tabindex. */ + @Prop({ mutable: true }) rovingTabIndex = 0; + + /** Renders as data-test on the button element. */ + @Prop() testId?: string; + + /** The unique value of this segment within the group. */ + @Prop() value!: string; + + /** Spread onto the native button element. */ + @Prop() nativeAttributes?: { [key: string]: string }; + + /** @internal Emitted when clicked. Consumed by cat-segmented-control. */ + @Event() catSegmentClick!: EventEmitter; + + /** @internal Emitted when segment receives focus. Consumed by cat-segmented-control. */ + @Event() catSegmentFocus!: EventEmitter; + + /** @internal Emitted when segment loses focus. Consumed by cat-segmented-control. */ + @Event() catSegmentBlur!: EventEmitter; + + /** Sets focus on the segment button. */ + @Method() + async doFocus(): Promise { + this.button?.focus(); + } + + /** Removes focus from the segment button. */ + @Method() + async doBlur(): Promise { + this.button?.blur(); + } + + render() { + const showIcon = !!this.icon; + const showLabel = !this.iconOnly || !this.icon; + + return ( + + + + ); + } + + private get iconSize(): SegmentSize { + const sizes: Record = { xs: 's', s: 's', m: 's', l: 'm', xl: 'l' }; + return sizes[this.size] ?? 's'; + } + + private handleClick() { + if (!this.disabled && !this.active) { + this.catSegmentClick.emit(this.value); + } + } + + private handleFocus() { + if (!this.disabled) { + this.catSegmentFocus.emit(this.value); + } + } + + private handleBlur() { + if (!this.disabled) { + this.catSegmentBlur.emit(this.value); + } + } +} diff --git a/core/src/components/cat-segment/readme.md b/core/src/components/cat-segment/readme.md new file mode 100644 index 00000000..6751c0f2 --- /dev/null +++ b/core/src/components/cat-segment/readme.md @@ -0,0 +1,70 @@ +# cat-segment + + + + + + +## Overview + +A single segment within a cat-segmented-control. + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | ----------- | +| `a11yLabel` | `a11y-label` | Adds accessible label for the button that is only shown for screen readers. Typically, this label text replaces the visible text on the button for users who use assistive technology. | `string \| undefined` | `undefined` | +| `disabled` | `disabled` | Whether this segment is disabled. | `boolean` | `false` | +| `icon` | `icon` | Icon name to display (same registry as cat-button). | `string \| undefined` | `undefined` | +| `iconOnly` | `icon-only` | When true, hides the label and shows the icon only. Requires icon and a11y-label. | `boolean` | `false` | +| `nativeAttributes` | -- | Spread onto the native button element. | `undefined \| { [key: string]: string; }` | `undefined` | +| `testId` | `test-id` | Renders as data-test on the button element. | `string \| undefined` | `undefined` | +| `value` _(required)_ | `value` | The unique value of this segment within the group. | `string` | `undefined` | + + +## Methods + +### `doBlur() => Promise` + +Removes focus from the segment button. + +#### Returns + +Type: `Promise` + + + +### `doFocus() => Promise` + +Sets focus on the segment button. + +#### Returns + +Type: `Promise` + + + + +## Shadow Parts + +| Part | Description | +| ---------- | -------------------------- | +| `"button"` | The native button element. | + + +## Dependencies + +### Depends on + +- [cat-icon](../cat-icon) + +### Graph +```mermaid +graph TD; + cat-segment --> cat-icon + style cat-segment fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +Made with love in Hamburg, Germany diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-darwin.png new file mode 100644 index 00000000..e7641a6e Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-linux.png new file mode 100644 index 00000000..c4dd5d6b Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-group-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-darwin.png new file mode 100644 index 00000000..26df3ff2 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-linux.png new file mode 100644 index 00000000..abf474da Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-disabled-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-darwin.png new file mode 100644 index 00000000..eec4e326 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-linux.png new file mode 100644 index 00000000..8372884b Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-focus-on-group-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-darwin.png new file mode 100644 index 00000000..8204e53d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-linux.png new file mode 100644 index 00000000..8dc429e1 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-hover-on-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-darwin.png new file mode 100644 index 00000000..9855eadc Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-linux.png new file mode 100644 index 00000000..5d024c0d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-renders-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-darwin.png new file mode 100644 index 00000000..fb1246ca Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-linux.png new file mode 100644 index 00000000..fcac968c Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-select-second-segment-via-keyboard-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..22faba77 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..8427deea Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-1-chromium-darwin.png new file mode 100644 index 00000000..7254141d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-darwin.png new file mode 100644 index 00000000..9e91c698 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-linux.png new file mode 100644 index 00000000..9e91c698 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-icon-only-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-darwin.png new file mode 100644 index 00000000..7254141d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-linux.png new file mode 100644 index 00000000..a4ea467f Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-l-with-text-and-icon-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..79d936cc Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..9096b4b6 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-1-chromium-darwin.png new file mode 100644 index 00000000..5cad8330 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-darwin.png new file mode 100644 index 00000000..9b0fed57 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-linux.png new file mode 100644 index 00000000..9b0fed57 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-icon-only-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-darwin.png new file mode 100644 index 00000000..5cad8330 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-linux.png new file mode 100644 index 00000000..6d3414f5 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-m-with-text-and-icon-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..766c30a9 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..b50ea1bd Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-1-chromium-darwin.png new file mode 100644 index 00000000..cc77e261 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-darwin.png new file mode 100644 index 00000000..ba9b1a6d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-linux.png new file mode 100644 index 00000000..ba9b1a6d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-icon-only-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-darwin.png new file mode 100644 index 00000000..cc77e261 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-linux.png new file mode 100644 index 00000000..8d2a7613 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-s-with-text-and-icon-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..b331e4e3 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..927965fc Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-1-chromium-darwin.png new file mode 100644 index 00000000..dc43c5c7 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-darwin.png new file mode 100644 index 00000000..b18f92d6 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-linux.png new file mode 100644 index 00000000..b18f92d6 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-icon-only-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-darwin.png new file mode 100644 index 00000000..dc43c5c7 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-linux.png new file mode 100644 index 00000000..6cd4fd3b Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xl-with-text-and-icon-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..4e2e0496 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..13618d5d Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-1-chromium-darwin.png new file mode 100644 index 00000000..cb870424 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-darwin.png new file mode 100644 index 00000000..0f338ed6 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-linux.png new file mode 100644 index 00000000..0f338ed6 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-icon-only-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-darwin.png new file mode 100644 index 00000000..cb870424 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-linux.png new file mode 100644 index 00000000..285661bb Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-size-xs-with-text-and-icon-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-darwin.png new file mode 100644 index 00000000..1f26101b Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-linux.png new file mode 100644 index 00000000..f9ea58ce Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-active-segment-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-darwin.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-darwin.png new file mode 100644 index 00000000..0fc325fb Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-darwin.png differ diff --git a/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-linux.png b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-linux.png new file mode 100644 index 00000000..ef8fb4f9 Binary files /dev/null and b/core/src/components/cat-segmented-control/__screenshots__/cat-segmented-control.screenshot.tsx/cat-segmented-control-screenshot-with-overflowing-labels-1-chromium-linux.png differ diff --git a/core/src/components/cat-segmented-control/cat-segmented-control.e2e.tsx b/core/src/components/cat-segmented-control/cat-segmented-control.e2e.tsx new file mode 100644 index 00000000..253593d2 --- /dev/null +++ b/core/src/components/cat-segmented-control/cat-segmented-control.e2e.tsx @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest'; +import { render, h } from '@stencil/vitest'; + +describe('cat-segmented-control', () => { + it('renders hydrated', async () => { + const { root } = await render(); + await expect.element(root).toHaveClass('hydrated'); + }); + + it('moves selection with arrow keys and wraps at control boundaries', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + Option 3 + + ); + const segments = Array.from(root.querySelectorAll('cat-segment')); + const buttons = segments.map(segment => segment.shadowRoot?.querySelector('button')); + + buttons[0]?.focus(); + buttons[0]?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true })); + await waitForChanges(); + expect(root.value).toBe('two'); + expect(document.activeElement?.shadowRoot?.activeElement).toBe(buttons[1]); + expect(buttons[0]?.getAttribute('aria-checked')).toBe('false'); + expect(buttons[1]?.getAttribute('aria-checked')).toBe('true'); + + buttons[1]?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true })); + await waitForChanges(); + expect(root.value).toBe('one'); + expect(document.activeElement?.shadowRoot?.activeElement).toBe(buttons[0]); + expect(buttons[0]?.getAttribute('aria-checked')).toBe('true'); + expect(buttons[1]?.getAttribute('aria-checked')).toBe('false'); + + buttons[0]?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true })); + await waitForChanges(); + expect(root.value).toBe('three'); + expect(document.activeElement?.shadowRoot?.activeElement).toBe(buttons[2]); + expect(buttons[0]?.getAttribute('aria-checked')).toBe('false'); + expect(buttons[2]?.getAttribute('aria-checked')).toBe('true'); + }); + + it('skips disabled segments during keyboard navigation', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + Option 2 + + Option 3 + + ); + const segments = Array.from(root.querySelectorAll('cat-segment')); + const buttons = segments.map(segment => segment.shadowRoot?.querySelector('button')); + + buttons[0]?.focus(); + buttons[0]?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, composed: true })); + await waitForChanges(); + + expect(root.value).toBe('three'); + expect(buttons[2]?.getAttribute('aria-checked')).toBe('true'); + expect(buttons[1]?.getAttribute('aria-checked')).toBe('false'); + }); + + it('selects a segment when its button is clicked', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + + ); + const button = root + .querySelector('cat-segment:nth-of-type(2)') + ?.shadowRoot?.querySelector('button'); + const changeListener = (event: Event) => { + expect((event as CustomEvent).detail).toBe('two'); + }; + root.addEventListener('catChange', changeListener); + + await button?.click(); + await waitForChanges(); + + expect(root.value).toBe('two'); + expect(button?.getAttribute('aria-checked')).toBe('true'); + root.removeEventListener('catChange', changeListener); + }); + + it('forwards focus and blur events from segments', async () => { + const { root } = await render( + + Option 1 + + ); + const events: { type: string; detail: string }[] = []; + root.addEventListener('catFocus', event => { + events.push({ type: 'focus', detail: (event as CustomEvent).detail }); + }); + root.addEventListener('catBlur', event => { + events.push({ type: 'blur', detail: (event as CustomEvent).detail }); + }); + const button = root.querySelector('cat-segment')?.shadowRoot?.querySelector('button'); + + button?.focus(); + button?.blur(); + + expect(events).toEqual([ + { type: 'focus', detail: 'one' }, + { type: 'blur', detail: 'one' } + ]); + }); +}); diff --git a/core/src/components/cat-segmented-control/cat-segmented-control.screenshot.tsx b/core/src/components/cat-segmented-control/cat-segmented-control.screenshot.tsx new file mode 100644 index 00000000..dbd58281 --- /dev/null +++ b/core/src/components/cat-segmented-control/cat-segmented-control.screenshot.tsx @@ -0,0 +1,200 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { h, render } from '@stencil/vitest'; +import { page, userEvent } from '@vitest/browser/context'; + +describe('cat-segmented-control screenshot', () => { + const SIZES = ['xs', 's', 'm', 'l', 'xl'] as const; + + const WRAPPER_STYLE = { + padding: '20px', + width: '760px', + display: 'inline-block', + backgroundColor: 'white' + }; + + beforeEach(async () => { + await page.viewport(800, 200); + }); + + afterEach(async () => { + // Reset mouse position after each test to prevent hover state leaking into the next test. + await page.elementLocator(document.body).hover(); + }); + + it('renders', async () => { + const { root } = await render( +
+ + Option 1 + Option 2 + Option 3 + +
+ ); + await expect(root).toMatchScreenshot(); + }); + + it('with overflowing labels', async () => { + const { root } = await render( +
+ + Lorem ipsum dolor sit amet consetetur sadipscing elitr + Averylongtextwithoutwhitespace + Short enough + +
+ ); + await expect(root).toMatchScreenshot(); + }); + + it('with active segment', async () => { + const { root } = await render( +
+ + Option 1 + Option 2 + Option 3 + +
+ ); + await expect(root).toMatchScreenshot(); + }); + + it('hover on segment', async () => { + const { root } = await render( +
+ + Option 1 + + Option 2 + + Option 3 + +
+ ); + + for (const segment of root.querySelectorAll('cat-segment')) { + const button = segment.shadowRoot?.querySelector('button[data-test="two-segment"]'); + if (button) { + await page.elementLocator(button!).hover(); + break; + } + } + + await expect(root).toMatchScreenshot(); + }); + + it('focus on group', async () => { + const { root } = await render( +
+ + Option 1 + Option 2 + Option 3 + +
+ ); + + // tab to move focus into the group, which will focus the first segment + await userEvent.tab(); + + await expect(root).toMatchScreenshot(); + }); + + it('select second segment via keyboard', async () => { + const { root } = await render( +
+ + Option 1 + Option 2 + Option 3 + +
+ ); + + // tab to move focus into the group, which will focus the first segment + await userEvent.tab(); + // select the second segment via keyboard + await userEvent.keyboard('{ArrowRight}'); + + await expect(root).toMatchScreenshot(); + }); + + it('disabled group', async () => { + const { root } = await render( +
+ + Option 1 + Option 2 + Option 3 + +
+ ); + + await expect(root).toMatchScreenshot(); + }); + + it('disabled segment', async () => { + const { root } = await render( +
+ + Option 1 + + Option 2 + + Option 3 + +
+ ); + + await expect(root).toMatchScreenshot(); + }); + + it.each(SIZES)('size %s with active segment', async size => { + const { root } = await render( +
+ + Option 1 (size {size.toLocaleUpperCase()}) + Option 2 (size {size.toLocaleUpperCase()}) + Option 3 (size {size.toLocaleUpperCase()}) + +
+ ); + + await expect(root).toMatchScreenshot(); + }); + + it.each(SIZES)('size %s with text and icon', async size => { + const { root } = await render( +
+ + + Option 1 (size {size.toLocaleUpperCase()}) + + + Option 2 (size {size.toLocaleUpperCase()}) + + + Option 3 (size {size.toLocaleUpperCase()}) + + +
+ ); + + await expect(root).toMatchScreenshot(); + }); + + it.each(SIZES)('size %s with icon only', async size => { + const { root } = await render( +
+ + + + + +
+ ); + + await expect(root).toMatchScreenshot(); + }); +}); diff --git a/core/src/components/cat-segmented-control/cat-segmented-control.scss b/core/src/components/cat-segmented-control/cat-segmented-control.scss new file mode 100644 index 00000000..77d9c575 --- /dev/null +++ b/core/src/components/cat-segmented-control/cat-segmented-control.scss @@ -0,0 +1,19 @@ +@use 'variables' as *; +@use 'mixins' as *; + +:host { + display: inline-flex; +} + +:host([hidden]) { + display: none; +} + +.cat-segmented-control { + display: inline-flex; + align-items: center; + background: cat-token('color.ui.background.muted'); + border-radius: cat-border-radius('l'); + padding: 0.25rem; + gap: 0.25rem; +} diff --git a/core/src/components/cat-segmented-control/cat-segmented-control.spec.tsx b/core/src/components/cat-segmented-control/cat-segmented-control.spec.tsx new file mode 100644 index 00000000..5856cf4d --- /dev/null +++ b/core/src/components/cat-segmented-control/cat-segmented-control.spec.tsx @@ -0,0 +1,279 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render } from '@stencil/vitest'; +import { h } from '@stencil/core'; +import './cat-segmented-control'; + +describe('cat-segmented-control', () => { + it('renders', async () => { + const { root } = await render( + + Option 1 + Option 2 + + ); + + expect(root).toEqualLightHtml(` + + + Option 1 + + + Option 2 + + + `); + }); + + it('handles a segment selection event', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + Option 2 + + + ); + + const changeListener = vi.fn(); + root.addEventListener('catChange', changeListener); + + const segment = root.querySelector('[test-id="optionTwo"]'); + + expect(segment).not.toBeNull(); + segment?.dispatchEvent(new CustomEvent('catSegmentClick', { detail: 'two', bubbles: true, composed: true })); + await waitForChanges(); + + expect(changeListener).toHaveBeenCalledWith(expect.objectContaining({ detail: 'two' })); + }); + + it('emits the focused segment value', async () => { + const { root } = await render( + + Option 1 + + ); + const focusListener = vi.fn(); + root.addEventListener('catFocus', focusListener); + + root.querySelector('cat-segment')?.dispatchEvent( + new CustomEvent('catSegmentFocus', { + detail: 'one', + bubbles: true, + composed: true + }) + ); + + expect(focusListener).toHaveBeenCalledOnce(); + expect(focusListener.mock.calls[0][0]).toMatchObject({ detail: 'one' }); + }); + + it('emits the blurred segment value', async () => { + const { root } = await render( + + Option 1 + + ); + const blurListener = vi.fn(); + root.addEventListener('catBlur', blurListener); + + root.querySelector('cat-segment')?.dispatchEvent( + new CustomEvent('catSegmentBlur', { + detail: 'one', + bubbles: true, + composed: true + }) + ); + + expect(blurListener).toHaveBeenCalledOnce(); + expect(blurListener.mock.calls[0][0]).toMatchObject({ detail: 'one' }); + }); + + it('renders group accessibility and native attributes', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + ); + + await waitForChanges(); + const group = root.shadowRoot?.querySelector('[role="radiogroup"]'); + + expect(group?.getAttribute('aria-label')).toBe('Test aria label'); + expect(group?.getAttribute('data-test')).toBe('test-id'); + expect(group?.getAttribute('data-context')).toBe('settings'); + }); + + it('renders radio state and roving tabindex', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + + ); + + await waitForChanges(); + + const segments = Array.from(root.querySelectorAll('cat-segment')); + expect(root.value).toBe('two'); + expect(segments).toHaveLength(2); + }); + + it('preserves required group semantics over native attributes', async () => { + const { root } = await render( + + Option 1 + + ); + const group = root.shadowRoot?.querySelector('div'); + + expect(group?.getAttribute('role')).toBe('radiogroup'); + expect(group?.getAttribute('aria-label')).toBe('Required label'); + expect(group).toHaveClass('cat-segmented-control'); + }); + + it('propagates size to all segments', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + + ); + + await waitForChanges(); + + const sizes = Array.from(root.querySelectorAll('cat-segment')).map(segment => segment.size); + expect(sizes).toEqual(['xl', 'xl']); + }); + + it('disables all segments when control is disabled', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + + ); + + await waitForChanges(); + + expect( + Array.from(root.querySelectorAll('cat-segment')).every(segment => segment.disabled) + ).toBe(true); + }); + + it('restores segments when control is re-enabled', async () => { + const { root, waitForChanges } = await render( + + Option 1 + Option 2 + + ); + + await waitForChanges(); + root.disabled = true; + await waitForChanges(); + root.disabled = false; + await waitForChanges(); + + expect( + Array.from(root.querySelectorAll('cat-segment')).every(segment => !segment.disabled) + ).toBe(true); + }); + + it('preserves segment-level disabled state when control is re-enabled', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + ); + + await waitForChanges(); + const segment = root.querySelector('cat-segment'); + segment!.disabled = true; + await waitForChanges(); + + root.disabled = true; + await waitForChanges(); + root.disabled = false; + await waitForChanges(); + + expect(segment?.disabled).toBe(true); + }); + + it('ignores child-list mutations inside segments', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + ); + + await waitForChanges(); + const segment = root.querySelector('cat-segment'); + segment!.size = 'xl'; + segment!.append(document.createElement('span')); + await waitForChanges(); + + expect(segment?.size).toBe('xl'); + }); + + it('does not prevent arrow-key default when all segments are disabled', async () => { + const { root, waitForChanges } = await render( + + + Option 1 + + + ); + + await waitForChanges(); + const event = new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true }); + + root.dispatchEvent(event); + + expect(event.defaultPrevented).toBe(false); + }); + + it('does not prevent arrow-key default when focus is outside enabled segments', async () => { + const { root, waitForChanges } = await render( + + Option 1 + + ); + + await waitForChanges(); + const segment = root.querySelector('cat-segment'); + segment!.doFocus = vi.fn(); + const outside = document.createElement('button'); + document.body.append(outside); + outside.focus(); + const event = new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true, cancelable: true }); + + root.dispatchEvent(event); + + expect(event.defaultPrevented).toBe(false); + expect(segment!.doFocus).not.toHaveBeenCalled(); + outside.remove(); + }); + + it('does not emit selection change for focus or blur events', async () => { + const { root } = await render( + + Option 1 + + ); + const changeListener = vi.fn(); + root.addEventListener('catChange', changeListener); + const segment = root.querySelector('cat-segment'); + + segment?.dispatchEvent(new CustomEvent('catSegmentFocus', { detail: 'one', bubbles: true, composed: true })); + segment?.dispatchEvent(new CustomEvent('catSegmentBlur', { detail: 'one', bubbles: true, composed: true })); + + expect(changeListener).not.toHaveBeenCalled(); + }); +}); diff --git a/core/src/components/cat-segmented-control/cat-segmented-control.tsx b/core/src/components/cat-segmented-control/cat-segmented-control.tsx new file mode 100644 index 00000000..7a004e3d --- /dev/null +++ b/core/src/components/cat-segmented-control/cat-segmented-control.tsx @@ -0,0 +1,194 @@ +import { Component, Element, Event, EventEmitter, h, Host, Listen, Prop, Watch } from '@stencil/core'; + +/** + * A segmented control for selecting one option from a mutually-exclusive set. + * + * @part group - The grouping wrapper element. + */ +@Component({ + tag: 'cat-segmented-control', + styleUrl: 'cat-segmented-control.scss', + shadow: true +}) +export class CatSegmentedControl { + private segments: HTMLCatSegmentElement[] = []; + private readonly disabledByGroup = new WeakSet(); + private mutationObserver?: MutationObserver; + + @Element() hostElement!: HTMLElement; + + /** + * The value of the currently selected segment. + */ + @Prop({ mutable: true }) value?: string; + + /** + * The size of the control, propagated to all child segments. + */ + @Prop() size: 'xs' | 's' | 'm' | 'l' | 'xl' = 'm'; + + /** + * Whether the entire control is disabled. + */ + @Prop() disabled = false; + + /** + * Accessible label for the group (screen readers only). + */ + @Prop({ attribute: 'a11y-label' }) a11yLabel?: string; + + /** + * Renders as data-test on the root element. + */ + @Prop() testId?: string; + + /** + * Spread onto the root element. + */ + @Prop() nativeAttributes?: { [key: string]: string }; + + /** + * Emitted when the selected segment changes. Payload is the new segment value. + */ + @Event() catChange!: EventEmitter; + + /** + * Emitted when a segment gains focus. Payload is the segment value. + */ + @Event() catFocus!: EventEmitter; + + /** + * Emitted when a segment loses focus. Payload is the segment value. + */ + @Event() catBlur!: EventEmitter; + + @Watch('value') + onValueChange(newValue?: string) { + this.segments.forEach(s => (s.active = s.value === newValue)); + this.updateTabIndex(); + } + + @Watch('size') + onSizeChange(newSize: CatSegmentedControl['size']) { + this.segments.forEach(s => (s.size = newSize)); + } + + @Watch('disabled') + onDisabledChange(disabled: boolean) { + if (disabled) { + this.segments.forEach(segment => { + if (!segment.disabled) { + this.disabledByGroup.add(segment); + segment.disabled = true; + } + }); + return; + } + + this.segments.forEach(segment => { + if (this.disabledByGroup.has(segment)) { + this.disabledByGroup.delete(segment); + segment.disabled = false; + } + }); + } + + componentDidLoad() { + this.sync(); + this.mutationObserver = new MutationObserver(mutations => { + if (mutations.some(mutation => this.isSegmentMutation(mutation))) { + this.sync(); + } + }); + this.mutationObserver.observe(this.hostElement, { childList: true, attributes: true, subtree: true }); + } + + disconnectedCallback() { + this.mutationObserver?.disconnect(); + } + + @Listen('catSegmentClick') + onSegmentClick(event: CustomEvent) { + this.selectSegment(event.detail); + } + + @Listen('catSegmentFocus') + onSegmentFocus(event: CustomEvent) { + this.catFocus.emit(event.detail); + } + + @Listen('catSegmentBlur') + onSegmentBlur(event: CustomEvent) { + this.catBlur.emit(event.detail); + } + + @Listen('keydown') + onKeydown(event: KeyboardEvent) { + const keys = ['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp']; + if (!keys.includes(event.key)) return; + + const enabled = this.segments.filter(s => !s.disabled); + const activeSegment = this.segments.find(segment => event.composedPath().includes(segment)); + const idx = enabled.indexOf(activeSegment!); + if (enabled.length === 0 || idx === -1) return; + + const offset = ['ArrowRight', 'ArrowDown'].includes(event.key) ? 1 : -1; + const target = enabled[(idx + offset + enabled.length) % enabled.length]; + this.selectSegment(target.value); + target.doFocus(); + event.preventDefault(); + } + + render() { + return ( + +
+ +
+
+ ); + } + + private sync() { + this.segments = Array.from(this.hostElement.querySelectorAll('cat-segment')); + this.onValueChange(this.value); + this.onSizeChange(this.size); + this.onDisabledChange(this.disabled); + } + + private isSegmentMutation(mutation: MutationRecord): boolean { + if (mutation.type === 'attributes') { + return mutation.target.nodeName === 'CAT-SEGMENT'; + } + + return [...mutation.addedNodes, ...mutation.removedNodes].some(node => { + if (node.nodeType !== Node.ELEMENT_NODE) return false; + + const element = node as Element; + return element.matches('cat-segment') || element.querySelector('cat-segment') !== null; + }); + } + + private updateTabIndex() { + const enabled = this.segments.filter(s => !s.disabled); + const active = enabled.find(s => s.active) ?? enabled[0]; + + this.segments.forEach(segment => (segment.rovingTabIndex = segment === active ? 0 : -1)); + } + + private selectSegment(value: string) { + if (this.value === value) return; + + this.value = value; + this.segments.forEach(segment => (segment.active = segment.value === value)); + this.updateTabIndex(); + this.catChange.emit(value); + } +} diff --git a/core/src/components/cat-segmented-control/readme.md b/core/src/components/cat-segmented-control/readme.md new file mode 100644 index 00000000..dda65a19 --- /dev/null +++ b/core/src/components/cat-segmented-control/readme.md @@ -0,0 +1,42 @@ +# cat-segmented-control + + + + + + +## Overview + +A segmented control for selecting one option from a mutually-exclusive set. + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------ | ------------ | ---------------------------------------------------------- | ----------------------------------------- | ----------- | +| `a11yLabel` | `a11y-label` | Accessible label for the group (screen readers only). | `string \| undefined` | `undefined` | +| `disabled` | `disabled` | Whether the entire control is disabled. | `boolean` | `false` | +| `nativeAttributes` | -- | Spread onto the root element. | `undefined \| { [key: string]: string; }` | `undefined` | +| `size` | `size` | The size of the control, propagated to all child segments. | `"l" \| "m" \| "s" \| "xl" \| "xs"` | `'m'` | +| `testId` | `test-id` | Renders as data-test on the root element. | `string \| undefined` | `undefined` | +| `value` | `value` | The value of the currently selected segment. | `string \| undefined` | `undefined` | + + +## Events + +| Event | Description | Type | +| ----------- | ---------------------------------------------------------------------------- | --------------------- | +| `catBlur` | Emitted when a segment loses focus. Payload is the segment value. | `CustomEvent` | +| `catChange` | Emitted when the selected segment changes. Payload is the new segment value. | `CustomEvent` | +| `catFocus` | Emitted when a segment gains focus. Payload is the segment value. | `CustomEvent` | + + +## Shadow Parts + +| Part | Description | +| --------- | ----------------------------- | +| `"group"` | The grouping wrapper element. | + + +---------------------------------------------- + +Made with love in Hamburg, Germany diff --git a/core/src/index.html b/core/src/index.html index 3fbb5657..3b658d3e 100644 --- a/core/src/index.html +++ b/core/src/index.html @@ -253,6 +253,7 @@

Catalyst Web Components

  • Pagination
  • Radio
  • Scrollable
  • +
  • Segmented Control
  • Select
  • Skeleton
  • Spinner
  • @@ -358,11 +359,11 @@

    Size

    Size XL

    - sdfdsf sdfsdf - sdfdsf sdfsdf - sdfdsf sdfsdf - sdfdsf sdfsdf - sdfdsf sdfsdf + sdfdsf sdfsdf + sdfdsf sdfsdf + sdfdsf sdfsdf + sdfdsf sdfsdf + sdfdsf sdfsdf

    Round

    Round @@ -446,8 +447,8 @@

    Links

    Google (ext.)
    - Link 1Link 2 + Link 1 + Link 2

    Active

    @@ -457,8 +458,8 @@

    Active



    Secondary Secondary + >Secondary + Secondary

    Info @@ -483,11 +484,11 @@

    Disabled

    Primary

    Secondary + >Secondary + Secondary + >Secondary + Secondary

    Info @@ -618,8 +619,8 @@

    Navigation

    • Nav item 3 + >Nav item 3 +
    @@ -676,8 +677,8 @@

    Dropdown

    Button with very long content so that it is very wide + >Button with very long content so that it is very wide + -
  • Saved
  • -
  • Files
  • +
  • + Saved +
  • +
  • + Files +

  • Platform
  • -
  • Administration
  • -
  • Analytics
  • +
  • + Administration +
  • +
  • + Analytics +
  • Manage hero teaser
  • -
  • Reported content
  • +
  • + Reported content +

  • Settings
  • -
  • Account
  • -
  • Notifications
  • +
  • + Account +
  • +
  • + Notifications +

  • Support
  • -
  • Restart tour
  • -
  • Help
  • -
  • About
  • +
  • + Restart tour +
  • +
  • + Help +
  • +
  • + About +

  • -
  • Log out
  • +
  • + Log out +
  • @@ -759,8 +782,12 @@

    Dropdown

    Click me @@ -782,8 +809,8 @@

    Dropdown

  • Click one more time + >Click one more time + @@ -837,8 +864,8 @@

    Menu

    Nav item 5 (long)
  • Nsdhfgjhsfg js fgjsdhgf jshdgf sdhgf jsdgf jhsdg fjhsdg fjhsgd fjshgdfjs dfgjsd f + >Nsdhfgjhsfg js fgjsdhgf jshdgf sdhgf jsdgf jhsdg fjhsdg fjhsgd fjshgdfjs dfgjsd f +
    Nav item 3 @@ -1106,6 +1133,7 @@

    Notification

    Show Notifications