Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,34 @@ Wrapper that joins filters into a connected button group with collapsed borders
- `required: boolean = false`
- `invalid: boolean = false`

### Slider
**Selector:** `tedi-slider` | ControlValueAccessor
**Model:** `value: number`
**Inputs:**
- `inputId: string` (required) — id for the range input + label association
- `label: string`, `hideLabel: boolean | "keep-space" = false`
- `min: number = 0`, `max: number = 100`, `step: number = 1`
- `name: string`
- `minLabel: string`, `maxLabel: string` — text flanking the track
- `showCurrentValue: boolean = false` — render the current value on the right instead of `maxLabel`
- `valueFormatter: (value: number) => string` — formats the tooltip + current-value label
- `tooltip: boolean = true` — live-value tooltip above the thumb (while hovered/focused/dragged)
- `disabled: boolean = false`, `required: boolean = false`, `invalid: boolean = false`
- `feedbackText: ComponentInputs<FeedbackTextComponent>` — helper/error below
- `ariaLabel`, `ariaLabelledby`, `ariaValuetext: string`
**Content projection:** `[sliderAddon]` — a right-hand element, typically a `tedi-number-field` editing the same value.

```html
<tedi-slider inputId="volume" label="Volume" [formControl]="volume"
minLabel="0%" maxLabel="100%" [max]="100" />

<!-- Paired with a number field editing the same value -->
<tedi-slider inputId="pct" label="Percent" [(value)]="pct" minLabel="0%" [showCurrentValue]="true"
[valueFormatter]="formatPct">
<tedi-number-field sliderAddon inputId="pct-field" [(value)]="pct" suffix="%" />
</tedi-slider>
```

### Checkbox
**Selector:** `input[type=checkbox][tedi-checkbox]`
**Inputs:**
Expand Down Expand Up @@ -1865,7 +1893,17 @@ The `[(open)]` binding approach is deprecated. Use `ModalService.open()` for new
**Inputs:**
- `position: TooltipPosition = "top"`
- `preventOverflow: boolean = true`
- `openWith: TooltipOpenWith = "both"` — hover, focus, or both
- `openWith: TooltipOpenWith = "both"` — `"hover"`, `"click"`, `"both"`, or `"none"`. Use `"none"` to disable the built-in triggers and control visibility yourself via `open`.
- `timeoutDelay: number = 100` — ms before closing when the pointer leaves the trigger/content
- `offset: number = 4` — extra distance (px) between the tooltip and its trigger, on top of the arrow allowance. Set `0` to sit the tooltip directly against the trigger (e.g. a slider thumb).
- `trackPosition: boolean = false` — while open, follow a moving origin (e.g. a dragging handle) with a `requestAnimationFrame` reposition loop. Enable only while the origin can actually move.
**Models:**
- `open: boolean | undefined` — controlled open state (two-way). Leave unset for the default trigger-driven behavior; typically paired with `openWith="none"`.
**Methods:**
- `updatePosition()` — manually reposition against the origin (the imperative alternative to `trackPosition`).

**`tedi-tooltip-trigger` inputs:**
- `interactive: boolean = true` — when `false`, skips focus/`tabindex`/`aria-describedby` synthesis so the trigger is a pure positioning anchor for a decorative or externally-controlled element (focus/ARIA live elsewhere). The overlay anchors to the trigger's host element.

```html
<tedi-tooltip position="top">
Expand All @@ -1874,6 +1912,14 @@ The `[(open)]` binding approach is deprecated. Use `ModalService.open()` for new
</tedi-tooltip-trigger>
<tedi-tooltip-content>Tooltip text</tedi-tooltip-content>
</tedi-tooltip>

<!-- Controlled + following a custom draggable element -->
<tedi-tooltip openWith="none" [open]="isDragging" [trackPosition]="isDragging">
<tedi-tooltip-trigger [interactive]="false">
<span class="my-handle" aria-hidden="true"></span>
</tedi-tooltip-trigger>
<tedi-tooltip-content>{{ value }}</tedi-tooltip-content>
</tedi-tooltip>
```

## Tags
Expand Down
1 change: 1 addition & 0 deletions tedi/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./feedback-text/feedback-text.component";
export * from "./label/label.component";
export * from "./number-field/number-field.component";
export * from "./select";
export * from "./slider/slider.component";
export * from "./toggle/toggle.component";
export * from "./form-field/form-field.component";
export * from "./form-field/form-field-control";
Expand Down
1 change: 1 addition & 0 deletions tedi/components/form/slider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./slider.component";
76 changes: 76 additions & 0 deletions tedi/components/form/slider/slider.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@if (label()) {
<label
tedi-label
[for]="inputId()"
[required]="required()"
[class.sr-only]="hideLabel() === true"
[class.tedi-slider__label--reserve-space]="hideLabel() === 'keep-space'"
>{{ label() }}</label>
}
Comment thread
mart-sessman marked this conversation as resolved.
<div class="tedi-slider__container">
<div class="tedi-slider__track-row">
@if (minLabel() != null) {
<span class="tedi-slider__range-label" aria-hidden="true">{{ minLabel() }}</span>
}
<div class="tedi-slider__track" [style]="progressStyle()">
<input
type="range"
class="tedi-slider__input"
[id]="inputId()"
[attr.name]="name()"
[attr.min]="min()"
[attr.max]="max()"
[attr.step]="step()"
[value]="clampedValue()"
[disabled]="isDisabled()"
[required]="required()"
[attr.aria-invalid]="isInvalid() ? 'true' : null"
[attr.aria-describedby]="feedbackId()"
[attr.aria-label]="ariaLabel()"
[attr.aria-labelledby]="ariaLabelledby()"
[attr.aria-valuetext]="ariaValuetext()"
(input)="handleInput($event)"
(mouseenter)="handleMouseEnter()"
(mouseleave)="handleMouseLeave()"
(pointerdown)="handlePointerDown()"
(focus)="handleFocus()"
(blur)="handleBlur()"
/>
@if (canShowTooltip()) {
<tedi-tooltip
openWith="none"
position="top"
[offset]="0"
[open]="tooltipOpen()"
[trackPosition]="isDragging() || isFocused()"
>
<tedi-tooltip-trigger
class="tedi-slider__thumb-anchor"
[interactive]="false"
></tedi-tooltip-trigger>
<tedi-tooltip-content>{{ formattedValue() }}</tedi-tooltip-content>
</tedi-tooltip>
}
</div>
@if (rightLabel() != null) {
<span
class="tedi-slider__range-label"
[attr.aria-hidden]="showCurrentValue() ? null : 'true'"
[attr.aria-live]="showCurrentValue() ? 'polite' : null"
>
{{ rightLabel() }}
</span>
}
</div>
<div class="tedi-slider__addon">
<ng-content select="[sliderAddon]" />
</div>
</div>
@if (feedbackText(); as feedback) {
<tedi-feedback-text
[attr.id]="feedbackId()"
[text]="feedback.text"
[type]="feedback.type"
[position]="feedback.position"
/>
}
205 changes: 205 additions & 0 deletions tedi/components/form/slider/slider.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
.tedi-slider {
display: flex;
flex-direction: column;
width: 100%;
}

.tedi-slider__label--reserve-space {
min-height: var(--body-regular-line-height);
overflow: hidden;
clip-path: inset(50%);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

.tedi-slider__container {
display: flex;
flex-wrap: wrap;
gap: var(--form-slider-inner-spacing);
align-items: center;
width: 100%;
}

// added min-width so that the slider wouldn't squash and addon would move to next line
.tedi-slider__container:has(.tedi-slider__addon:not(:empty)) .tedi-slider__track-row {
min-width: 8rem;
}

.tedi-slider__track-row {
display: flex;
flex: 1 0 0;
gap: var(--layout-grid-gutters-08);
align-items: center;
min-width: 0;
}

.tedi-slider__range-label {
flex-shrink: 0;
font-size: var(--body-regular-size);
line-height: var(--body-regular-line-height);
color: var(--form-slider-range-label-text);
white-space: nowrap;
}

.tedi-slider__track {
--tedi-slider-progress: 0%;
--tedi-slider-progress-ratio: 0;

position: relative;
flex: 1 0 0;
min-width: 0;
height: var(--form-slider-height);
background: linear-gradient(to right,
var(--form-slider-active-background-default) 0%,
var(--form-slider-active-background-default) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) 100%);
border: 1px solid var(--form-slider-border-default);
border-radius: var(--form-slider-radius);
}

.tedi-slider__thumb-anchor {
position: absolute;
top: 50%;
left: calc(var(--form-slider-thumb-size) / 2 + var(--tedi-slider-progress-ratio) * (100% - var(--form-slider-thumb-size)));
display: block;
width: var(--form-slider-thumb-size);
height: var(--form-slider-thumb-size);
pointer-events: none;
transform: translate(-50%, -50%);
}

.tedi-slider__input {
--tedi-slider-hit-padding: 0.625rem;

position: absolute;
inset: calc(var(--tedi-slider-hit-padding) * -1) 0;
width: 100%;
padding: 0;
margin: 0;
appearance: none;
cursor: pointer;
outline: none;
background: transparent;
border: 0;
border-radius: inherit;

&::-webkit-slider-runnable-track {
width: 100%;
height: 100%;
background: transparent;
border: 0;
}

&::-moz-range-track {
width: 100%;
height: 100%;
background: transparent;
border: 0;
}

&::-webkit-slider-thumb {
width: var(--form-slider-thumb-size);
height: var(--form-slider-thumb-size);
margin-top: calc((var(--form-slider-height) - var(--form-slider-thumb-size)) / 2 + var(--tedi-slider-hit-padding));
appearance: none;
cursor: pointer;
background: var(--form-slider-thumb-background-default);
border: var(--tedi-borders-02) solid var(--form-slider-thumb-border-default);
border-radius: 50%;
}

&::-moz-range-thumb {
box-sizing: border-box;
width: var(--form-slider-thumb-size);
height: var(--form-slider-thumb-size);
cursor: pointer;
background: var(--form-slider-thumb-background-default);
border: var(--tedi-borders-02) solid var(--form-slider-thumb-border-default);
border-radius: 50%;
}

&:hover:not(:disabled)::-webkit-slider-thumb {
background: var(--form-slider-thumb-background-hover);
border-color: var(--form-slider-thumb-border-hover);
}

&:hover:not(:disabled)::-moz-range-thumb {
background: var(--form-slider-thumb-background-hover);
border-color: var(--form-slider-thumb-border-hover);
}

&:active:not(:disabled)::-webkit-slider-thumb {
background: var(--form-slider-thumb-background-active);
border-color: var(--form-slider-thumb-border-active);
}

&:active:not(:disabled)::-moz-range-thumb {
background: var(--form-slider-thumb-background-active);
border-color: var(--form-slider-thumb-border-active);
}

&:focus-visible:not(:disabled)::-webkit-slider-thumb {
background: var(--form-slider-thumb-background-focus);
border-color: var(--form-slider-thumb-border-focus);
box-shadow: 0 0 0 var(--tedi-borders-01) var(--tedi-neutral-100),
0 0 0 calc(var(--tedi-borders-01) + var(--tedi-borders-02)) var(--form-slider-thumb-border-focus);
}

&:focus-visible:not(:disabled)::-moz-range-thumb {
background: var(--form-slider-thumb-background-focus);
border-color: var(--form-slider-thumb-border-focus);
box-shadow: 0 0 0 var(--tedi-borders-01) var(--tedi-neutral-100),
0 0 0 calc(var(--tedi-borders-01) + var(--tedi-borders-02)) var(--form-slider-thumb-border-focus);
}
}

.tedi-slider__addon {
display: flex;
flex-shrink: 0;
align-items: center;

&:empty {
display: none;
}
}

.tedi-slider:hover:not(.tedi-slider--disabled) .tedi-slider__track {
background: linear-gradient(to right,
var(--form-slider-active-background-hover) 0%,
var(--form-slider-active-background-hover) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) 100%);
}

.tedi-slider--dragging:not(.tedi-slider--disabled) .tedi-slider__track {
background: linear-gradient(to right,
var(--form-slider-active-background-active) 0%,
var(--form-slider-active-background-active) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) 100%);
}

.tedi-slider--disabled {
.tedi-slider__track {
background: linear-gradient(to right,
var(--form-slider-active-background-disabled) 0%,
var(--form-slider-active-background-disabled) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) var(--tedi-slider-progress),
var(--form-slider-passive-background-default) 100%);
}

.tedi-slider__input {
cursor: not-allowed;

&::-webkit-slider-thumb {
cursor: not-allowed;
background: var(--form-slider-thumb-background-disabled);
border-color: var(--form-slider-thumb-border-disabled);
}

&::-moz-range-thumb {
cursor: not-allowed;
background: var(--form-slider-thumb-background-disabled);
border-color: var(--form-slider-thumb-border-disabled);
}
}
}
Loading
Loading