-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsegmented-control.tsx
More file actions
137 lines (117 loc) · 3.35 KB
/
segmented-control.tsx
File metadata and controls
137 lines (117 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import {
Component,
type ComponentInterface,
Element,
Event,
type EventEmitter,
Host,
h,
Listen,
Prop,
State,
Watch,
} from "@stencil/core";
export type Size = "small" | "medium";
/**
* The segmented control component presents a set of options where exactly one
* option is active at a time.
*
* The component displays an indicator under the selected option and emits a
* `scoutChange` event when the user picks a different option, so you can update
* `value`.
*
* Use button elements as the slotted segment options.
*/
@Component({
tag: "scout-segmented-control",
styleUrl: "segmented-control.css",
shadow: {
delegatesFocus: true,
},
})
export class ScoutSegmentedControl implements ComponentInterface {
/**
* Size of the input element. Large fields are typically used for prominent
* inputs, such as a top search field on a page, while medium fields are used
* for regular form inputs.
*/
@Prop() size: Size = "medium";
/**
* Zero-based index of the currently active segment.
*/
@Prop()
public value: number = 0;
/**
* Emitted when the active segment changes as a result of a user click.
* The `value` in the event detail is the zero-based index of the newly selected segment.
*/
@Event()
public scoutChange!: EventEmitter<{ value: number }>;
@State()
private widths: number[] = [];
@State()
private lefts: number[] = [];
@State()
private enableAnimations = false;
@Element() el!: HTMLElement;
render() {
const sizeClass = this.size === "small" ? "small" : "";
const noTransitionClass = this.enableAnimations ? "" : "no-transition";
return (
<Host class={`${sizeClass} ${noTransitionClass}`}>
<slot />
{this.getIndicator()}
</Host>
);
}
componentDidLoad() {
this.updateChildrenAttributes();
this.calculateIndicatorSizes();
requestAnimationFrame(() => {
this.enableAnimations = true;
});
}
getIndicator() {
const width = this.widths[this.value] || 0;
const left = this.lefts[this.value] || 0;
const indicatorStyle = {
width: `${width}px`,
transform: `translateX(${left}px)`,
};
return <div aria-hidden="true" class="indicator" style={indicatorStyle} />;
}
@Listen("click", { capture: true })
handleClick(event: MouseEvent) {
const target = event.target as HTMLElement;
const buttons = Array.from(this.el.children);
const clickedIndex = buttons.indexOf(target);
if (clickedIndex !== -1 && clickedIndex !== this.value) {
this.scoutChange.emit({ value: clickedIndex });
}
}
@Watch("value")
updateChildrenAttributes() {
Array.from(this.el.children).forEach((child, index) => {
const button = child as HTMLElement;
button.role = "radio";
if (index === this.value) {
button.ariaChecked = "true";
} else {
button.ariaChecked = "false";
}
});
}
@Watch("value")
calculateIndicatorSizes() {
// Get left padding of container
const baseLeft = parseFloat(getComputedStyle(this.el).paddingLeft) || 0;
this.widths = Array.from(this.el.children).map(
(child) => (child as HTMLElement).offsetWidth,
);
this.lefts = this.widths.map(
(_, index) =>
this.widths.slice(0, index).reduce((acc, w) => acc + w, 0) + baseLeft,
);
console.log("Calculated widths:", this.widths);
}
}