Skip to content

Commit 8aa7da6

Browse files
authored
Merge pull request #269 from US-CBP/feature/range-mvp
Feature/range mvp
2 parents 237a804 + 1ee4b61 commit 8aa7da6

File tree

11 files changed

+668
-117
lines changed

11 files changed

+668
-117
lines changed

packages/react-components/components/stencil-generated/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const CbpResizeObserver = /*@__PURE__*/createReactComponent<JSX.CbpResize
4949
export const CbpSection = /*@__PURE__*/createReactComponent<JSX.CbpSection, HTMLCbpSectionElement>('cbp-section');
5050
export const CbpSegmentedButtonGroup = /*@__PURE__*/createReactComponent<JSX.CbpSegmentedButtonGroup, HTMLCbpSegmentedButtonGroupElement>('cbp-segmented-button-group');
5151
export const CbpSkipNav = /*@__PURE__*/createReactComponent<JSX.CbpSkipNav, HTMLCbpSkipNavElement>('cbp-skip-nav');
52+
export const CbpSlider = /*@__PURE__*/createReactComponent<JSX.CbpSlider, HTMLCbpSliderElement>('cbp-slider');
5253
export const CbpStructuredList = /*@__PURE__*/createReactComponent<JSX.CbpStructuredList, HTMLCbpStructuredListElement>('cbp-structured-list');
5354
export const CbpStructuredListItem = /*@__PURE__*/createReactComponent<JSX.CbpStructuredListItem, HTMLCbpStructuredListItemElement>('cbp-structured-list-item');
5455
export const CbpSubnav = /*@__PURE__*/createReactComponent<JSX.CbpSubnav, HTMLCbpSubnavElement>('cbp-subnav');

packages/web-components/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This CHANGELOG.md tracks the updates to the web components package of the CBP de
55
The React components are wrappers generated from this package and will share the same changes. Projects using React 19 may use the native web components without React wrappers.
66

77
## [unreleased] TBD
8+
* First cut of the `cbp-slider` component for selecting a single value from a range.
89
* First cut of the `cbp-code-snippet` component, which is used to display code samples.
910
* Cleaned up the monorepo:
1011
* Removed vanilla package.

packages/web-components/src/components/cbp-form-field/cbp-form-field.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,20 +200,24 @@ cbp-form-field {
200200
font-style: italic;
201201
}
202202

203+
204+
// File Inputs
203205
input[type=file]{
204206
line-height: var(--cbp-space-8x);
205207
}
206208
::file-selector-button {
207209
display: none;
208210
}
209-
211+
212+
// Textarea
210213
textarea {
211214
min-height: 6.75rem;
212215
max-height: 80vh;
213216
padding-block: var(--cbp-space-2x);
214217
resize: vertical;
215218
}
216219

220+
// Select and Dropdowns
217221
select,
218222
cbp-dropdown .cbp-custom-form-control {
219223
appearance: none;

packages/web-components/src/components/cbp-form-field/cbp-form-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class CbpFormField {
159159
this.formField = this.host.querySelector('input,select,textarea');
160160

161161
// Treat nested components separately, as it's hard to modify their rendered content directly
162-
this.formFieldComponent = this.host.querySelector('cbp-dropdown');
162+
this.formFieldComponent = this.host.querySelector('cbp-dropdown,cbp-slider');
163163

164164
this.buttons = this.host.querySelectorAll('cbp-button');
165165
this.attachedButtons = this.host.querySelectorAll('[slot=cbp-form-field-attached-button] cbp-button');
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export default {
2+
title: 'Components/Form Fields',
3+
tags: ['beta'],
4+
argTypes: {
5+
label: {
6+
control: 'text',
7+
},
8+
description: {
9+
control: 'text',
10+
},
11+
fieldId: {
12+
control: 'text',
13+
},
14+
// min max step value
15+
error: {
16+
control: 'boolean',
17+
},
18+
readonly: {
19+
control: 'boolean',
20+
},
21+
disabled: {
22+
control: 'boolean',
23+
},
24+
context: {
25+
control: 'select',
26+
options: ['light-inverts', 'light-always', 'dark-inverts', 'dark-always'],
27+
},
28+
sx: {
29+
description: 'Supports adding inline styles as an object of key-value pairs comprised of CSS properties and values. Values should reference design tokens when possible.',
30+
control: 'object',
31+
},
32+
},
33+
args: {
34+
label: 'Field Label',
35+
description: 'Field description.',
36+
},
37+
};
38+
39+
const RangeTemplate = ({ label, description, fieldId, error, readonly, disabled, value, context, sx }) => {
40+
return `
41+
<cbp-form-field
42+
${label ? `label="${label}"` : ''}
43+
${description ? `description="${description}"` : ''}
44+
${fieldId ? `field-id="${fieldId}"` : ''}
45+
${error ? `error` : ''}
46+
${disabled ? `disabled` : ''}
47+
${readonly ? `readonly` : ''}
48+
${context && context != 'light-inverts' ? `context=${context}` : ''}
49+
${sx ? `sx=${JSON.stringify(sx)}` : ''}
50+
>
51+
<input
52+
type="range"
53+
name="range"
54+
${value ? `value="${value}"` : ''}
55+
/>
56+
</cbp-form-field>
57+
`;
58+
};
59+
60+
export const Range = RangeTemplate.bind({});
61+
Range.storyName = 'Range Slider (basic}';
62+
Range.args = {
63+
value: '',
64+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Meta, Markdown } from "@storybook/blocks";
2+
import Docs from './readme.md?raw';
3+
4+
<Meta title="Components/Slider/API Docs" />
5+
6+
<Markdown>{Docs}</Markdown>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/**
2+
* @prop --cbp-slider-thumb-color: var(--cbp-color-interactive-secondary-dark);
3+
* @prop --cbp-slider-thumb-color-hover: var(--cbp-color-interactive-secondary-darker);
4+
* @prop --cbp-slider-thumb-color-focus: var(--cbp-color-interactive-focus-dark);
5+
* @prop --cbp-slider-thumb-color-border: var(--cbp-color-background-light);
6+
* @prop --cbp-slider-thumb-color-border-focus: var(--cbp-color-white);
7+
* @prop --cbp-slider-thumb-color-halo: transparent;
8+
* @prop --cbp-slider-thumb-color-halo-hover: var(--cbp-color-interactive-secondary-darker);
9+
* @prop --cbp-slider-thumb-color-halo-focus: var(--cbp-color-interactive-focus-dark);
10+
* @prop --cbp-slider-track-color-selected: var(--cbp-color-interactive-selected-dark);
11+
* @prop --cbp-slider-track-color-unselected: var(--cbp-color-interactive-secondary-light);
12+
* @prop --cbp-slider-thumb-color-dark: var(--cbp-color-interactive-secondary-light);
13+
* @prop --cbp-slider-thumb-color-hover-dark: var(--cbp-color-interactive-secondary-lighter);
14+
* @prop --cbp-slider-thumb-color-focus-dark: var(--cbp-color-interactive-focus-light);
15+
* @prop --cbp-slider-thumb-color-border-dark: var(--cbp-color-background-dark);
16+
* @prop --cbp-slider-thumb-color-border-focus-dark: var(--cbp-color-black);
17+
* @prop --cbp-slider-thumb-color-halo-hover-dark: var(--cbp-color-interactive-secondary-lighter);
18+
* @prop --cbp-slider-thumb-color-halo-focus-dark: var(--cbp-color-interactive-focus-light);
19+
* @prop --cbp-slider-track-color-selected-dark: var(--cbp-color-interactive-selected-light);
20+
* @prop --cbp-slider-track-color-unselected-dark: var(--cbp-color-interactive-secondary-base);
21+
*/
22+
23+
:root {
24+
--cbp-slider-thumb-color: var(--cbp-color-interactive-secondary-dark);
25+
--cbp-slider-thumb-color-hover: var(--cbp-color-interactive-secondary-darker);
26+
--cbp-slider-thumb-color-focus: var(--cbp-color-interactive-focus-dark);
27+
--cbp-slider-thumb-color-border: var(--cbp-color-background-light);
28+
--cbp-slider-thumb-color-border-focus: var(--cbp-color-white);
29+
--cbp-slider-thumb-color-halo: transparent;
30+
--cbp-slider-thumb-color-halo-hover: var(--cbp-color-interactive-secondary-darker);
31+
--cbp-slider-thumb-color-halo-focus: var(--cbp-color-interactive-focus-dark);
32+
--cbp-slider-track-color-selected: var(--cbp-color-interactive-selected-dark);
33+
--cbp-slider-track-color-unselected: var(--cbp-color-interactive-secondary-light);
34+
35+
--cbp-slider-thumb-color-dark: var(--cbp-color-interactive-secondary-light);
36+
--cbp-slider-thumb-color-hover-dark: var(--cbp-color-interactive-secondary-lighter);
37+
--cbp-slider-thumb-color-focus-dark: var(--cbp-color-interactive-focus-light);
38+
--cbp-slider-thumb-color-border-dark: var(--cbp-color-background-dark);
39+
--cbp-slider-thumb-color-border-focus-dark: var(--cbp-color-black);
40+
--cbp-slider-thumb-color-halo-hover-dark: var(--cbp-color-interactive-secondary-lighter);
41+
--cbp-slider-thumb-color-halo-focus-dark: var(--cbp-color-interactive-focus-light);
42+
--cbp-slider-track-color-selected-dark: var(--cbp-color-interactive-selected-light);
43+
--cbp-slider-track-color-unselected-dark: var(--cbp-color-interactive-secondary-base);
44+
45+
--cbp-slider-track-selection-size: 0;
46+
}
47+
48+
// Displays dark design based on mode or context
49+
[data-cbp-theme=light] cbp-slider[context*=dark],
50+
[data-cbp-theme=dark] cbp-slider:not([context=dark-inverts]):not([context=light-always]) {
51+
--cbp-slider-thumb-color: var(--cbp-slider-thumb-color-dark);
52+
--cbp-slider-thumb-color-hover: var(--cbp-slider-thumb-color-hover-dark);
53+
--cbp-slider-thumb-color-focus: var(--cbp-slider-thumb-color-focus-dark);
54+
--cbp-slider-thumb-color-border: var(--cbp-slider-thumb-color-border-dark);
55+
--cbp-slider-thumb-color-border-focus: var(--cbp-slider-thumb-color-border-focus-dark);
56+
--cbp-slider-thumb-color-halo-hover: var(--cbp-slider-thumb-color-halo-hover-dark);
57+
--cbp-slider-thumb-color-halo-focus: var(--cbp-slider-thumb-color-halo-focus-dark);
58+
--cbp-slider-track-color-selected: var(--cbp-slider-track-color-selected-dark);
59+
--cbp-slider-track-color-unselected: var(--cbp-slider-track-color-unselected-dark);
60+
}
61+
62+
63+
cbp-slider {
64+
display: flex;
65+
align-items: center;
66+
gap: var(--cbp-space-2x);
67+
font-style: italic;
68+
69+
input[type=number] {
70+
width: 7ch;
71+
72+
// This removes the default increment arrows in the number input field
73+
&::-webkit-outer-spin-button,
74+
&::-webkit-inner-spin-button {
75+
-webkit-appearance: none;
76+
margin: 0; // Margins still appear even if hidden
77+
}
78+
}
79+
80+
input[type=number] {
81+
-moz-appearance: textfield; // Removes the default increment arrows in Firefox
82+
}
83+
84+
85+
.cbp-slider-wrapper {
86+
position: relative;
87+
display: flex;
88+
align-items: center;
89+
flex-grow: 1;
90+
width: 100%;
91+
height: 100%;
92+
93+
.cbp-slider-selection {
94+
position: absolute;
95+
display: block;
96+
background: var(--cbp-slider-track-color-selected);
97+
border-radius: var(--cbp-border-radius-pill);
98+
block-size: var(--cbp-space-2x);
99+
inline-size: 100%;
100+
transform-origin: left center;
101+
transform: scaleX(calc(var(--cbp-slider-track-selection-size) / 100)); // TechDebt: the scaling is impacting the border radius. Maybe revisit with clip-path to fix?
102+
pointer-events: none;
103+
}
104+
}
105+
106+
// Native range slider
107+
input[type=range] {
108+
-webkit-appearance: none;
109+
appearance: none;
110+
background: transparent;
111+
border: 0;
112+
outline: 0;
113+
margin: 0;
114+
padding: 0;
115+
cursor: pointer;
116+
//pointer-events: none; // this is used when 2 sliders are present for a range
117+
118+
// Track
119+
&::-webkit-slider-runnable-track{
120+
background: var(--cbp-slider-track-color-unselected);
121+
height: var(--cbp-space-half-x);
122+
border-radius: var(--cbp-border-radius-pill);
123+
}
124+
125+
&::-moz-range-track{
126+
background: var(--cbp-slider-track-color-unselected);
127+
height: var(--cbp-space-half-x);
128+
border-radius: var(--cbp-border-radius-pill);
129+
}
130+
131+
132+
// Thumb
133+
&::-webkit-slider-thumb {
134+
-webkit-appearance: none;
135+
appearance: none;
136+
margin-top: calc((var(--cbp-space-5x) / 2) * -1);
137+
height: var(--cbp-space-5x);
138+
width: var(--cbp-space-5x);
139+
background-color: var(--cbp-slider-thumb-color);
140+
border: var(--cbp-border-size-md) solid var(--cbp-slider-thumb-color-border);
141+
border-radius: var(--cbp-border-radius-circle);
142+
box-shadow: 0 0 0 var(--cbp-space-11x) color-mix(in srgb, var(--cbp-slider-thumb-color-halo), transparent 50%);
143+
clip-path: circle(122%); // verified
144+
pointer-events: all;
145+
}
146+
147+
&::-moz-range-thumb {
148+
-webkit-appearance: none;
149+
appearance: none;
150+
margin-top: calc((var(--cbp-space-4x) / 2) * -1);
151+
height: var(--cbp-space-5x);
152+
width: var(--cbp-space-5x);
153+
background-color: var(--cbp-slider-thumb-color);
154+
border: var(--cbp-border-size-md) solid var(--cbp-slider-thumb-color-border);
155+
border-radius: var(--cbp-border-radius-circle);
156+
box-shadow: 0 0 0 var(--cbp-space-11x) color-mix(in srgb, var(--cbp-slider-thumb-color-halo), transparent 50%);
157+
clip-path: circle(122%); // verified
158+
pointer-events: all;
159+
}
160+
161+
162+
// States
163+
&:hover:not(:disabled) {
164+
--cbp-slider-thumb-color-halo: var(--cbp-slider-thumb-color-halo-hover);
165+
--cbp-slider-thumb-color: var(--cbp-slider-thumb-color-hover);
166+
}
167+
168+
&:focus:not(:disabled) {
169+
--cbp-slider-thumb-color-halo: var(--cbp-slider-thumb-color-halo-focus);
170+
--cbp-slider-thumb-color: var(--cbp-slider-thumb-color-focus);
171+
--cbp-slider-thumb-color-border: var(--cbp-slider-thumb-color-border-focus);
172+
}
173+
174+
&:disabled {
175+
--cbp-slider-thumb-color: var(--cbp-color-interactive-secondary-dark);
176+
--cbp-slider-track-color-selected: var(--cbp-color-interactive-selected-dark);
177+
--cbp-slider-track-color-unselected: var(--cbp-color-interactive-secondary-light);
178+
--cbp-slider-thumb-color-dark: var(--cbp-color-interactive-disabled-light);
179+
--cbp-slider-track-color-selected-dark: var(--cbp-color-interactive-disabled-light);
180+
--cbp-slider-track-color-unselected-dark: var(--cbp-color-interactive-disabled-dark);
181+
cursor: not-allowed;
182+
}
183+
}
184+
185+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Meta } from '@storybook/addon-docs';
2+
3+
<Meta title="Components/Slider/Specifications" />
4+
5+
# cbp-slider
6+
7+
## Purpose
8+
9+
The primary use for the Slider component is to select a value within a range.
10+
11+
## Functional Requirements
12+
13+
* The Slider component wraps the slotted native form control (`input type="range"`) to provide custom, cross-browser styling and additional functionality.
14+
* The Slider component shall be slotted within the `cbp-form-field` component to supply an accessible label and description.
15+
* The component will show the min/max values to the left and right of the slider control by default, but these may be hidden.
16+
* The component will show a numeric input to the right of the slider by default, which is synced with the slider control. This control may be optionally hidden.
17+
* Optional named slots also exist before and after the slider control for adding custom content, such as icons or time-based values.
18+
19+
## Technical Specifications
20+
21+
### User Interactions
22+
23+
* The user interactions are that of a native `input type="range"` element:
24+
* Clicking anywhere on the form control or label text will place focus on the control.
25+
* Clicking on the "track" will select the corresponding value based on the position clicked on the slider.
26+
* Sliders are keyboard accessible by tabbing onto the control and then using the arrows.
27+
* Pressing the up or right arrow increments the slider by the step value (default = 1).
28+
* Pressing the down or left arrow decrements the slider by the step value (default = 1).
29+
* Optionally, a numeric input may be shown to the right of the slider.
30+
* Users can enter an exact numeric value into the input field.
31+
* This input is synchronized with the slider in terms of min, max, step, and value.
32+
* While the numeric input buttons are hidden, the up and down arrow keys may still be used to increments and decrement the value (by the step value) respectively.
33+
* This input has no `name` attribute and will not be submitted with a native form submission (the `input type=range` is submitted if it has a `name`.)
34+
35+
### Responsiveness
36+
37+
* The pattern label will wrap as needed.
38+
* The slider control is sized to 100% within a flexbox context and will shrink or grow based on available space.
39+
40+
### Accessibility
41+
42+
* The associated label and `aria-describedby` are provided by the parent `cbp-form-field` component.
43+
* The numeric input, when shown, has a static `aria-label` of "Slider value" and `aria-describedby` referencing the `label` element. While the pattern technically contains two inputs, the numeric input is more of an accessory, so a group/fieldset context was not used.
44+
* Full keyboard navigation is supported, as detailed under "User Interactions" above.
45+
* When using a mouse to click the thumb or track of the slider, the interactive element is not focused on Mac. Therefore, the component sets focus explicitly on click.
46+
47+
### Additional Notes and Considerations
48+
49+
* The primary use for the Slider component is to select a value within a range.
50+
* There are potentially other applications of this component such as a playback slider, but these will need to be discussed on a case by case basis and additional work may be needed to meet those requirements.
51+
* A slider should not be marked `readonly`, as its value can still be changed.
52+
* The native behavior of using a `datalist` to supply tick marks is not supported since we are overriding the CSS `appearance`.
53+
* TODO: The initial version of this component supports a single value. A range variation supporting two values (not natively supported by `input[type=range]`) is planned for the future.

0 commit comments

Comments
 (0)