Skip to content
Draft
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
40 changes: 40 additions & 0 deletions pages/slider/style-api-v2.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import { KeyValuePairs, Slider, SliderProps } from '~components';

import { SimplePage } from '../app/templates';

import styles from './style-api-v2.scss';

const styleClassNames = { root: styles.slider, track: styles.track, range: styles.range, handle: styles.handle };

export default function () {
const [value, setValue] = useState(40);
const slider = (props: Partial<SliderProps> & { ariaLabel: string }) => (
<Slider
value={value}
onChange={({ detail }) => setValue(detail.value)}
min={0}
max={100}
{...props}
{...{ styleClassNames }}
/>
);
return (
<SimplePage title="Slider - Style API v2" screenshotArea={{}}>
<KeyValuePairs
items={[
{
type: 'pair',
label: 'With tick marks',
value: slider({ ariaLabel: 'With tick marks', step: 20, tickMarks: true }),
},
{ type: 'pair', label: 'Disabled', value: slider({ ariaLabel: 'Disabled', disabled: true }) },
{ type: 'pair', label: 'Read-only', value: slider({ ariaLabel: 'Read-only', readOnly: true }) },
]}
/>
</SimplePage>
);
}
56 changes: 56 additions & 0 deletions pages/slider/style-api-v2.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

@use '@cloudscape-design/component-toolkit/styling' as styling;
@use '../style-api/palette' as palette;

.track {
@include styling.override {
background-color: palette.$surface-alt;
}
}

.range {
@include styling.override {
background-color: palette.$accent;
}
}

// The handle is the native <input type="range">, so its states are regular selectors.
.handle {
--awsui-style-handle-color: #{palette.$accent};
--awsui-style-border-radius: 4px;

&:hover {
--awsui-style-handle-color: #{palette.$accent-hover};
}

&:is(:focus, :active) {
--awsui-style-handle-color: #{palette.$accent-active};
}

&:disabled,
&[aria-disabled='true'] {
--awsui-style-handle-color: #{palette.$control-bg-disabled};
}
}

// The track and range don't contain the input, so they take its state through :has() on the root slot.
/* stylelint-disable selector-combinator-disallowed-list */
.slider {
&:has(.handle:is(:focus, :active)) .range {
@include styling.override {
background-color: palette.$accent-active;
}
}

// After the focus rule: a read-only slider stays focusable, and both rules have the same specificity.
&:has(.handle:disabled, .handle[aria-disabled='true']) .range {
@include styling.override {
background-color: palette.$control-bg-disabled;
}
}
}
/* stylelint-enable selector-combinator-disallowed-list */
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,48 @@ exports[`Style API docs definition for radio-button matches the snapshot: radio-
}
`;

exports[`Style API docs definition for slider matches the snapshot: slider 1`] = `
{
"slots": [
{
"name": "root",
"properties": [],
"propertyDescriptions": {},
"tokenDescriptions": {},
"tokens": [],
},
{
"name": "track",
"properties": [
"background-color",
],
"propertyDescriptions": {},
"tokenDescriptions": {},
"tokens": [],
},
{
"name": "range",
"properties": [
"background-color",
],
"propertyDescriptions": {},
"tokenDescriptions": {},
"tokens": [],
},
{
"name": "handle",
"properties": [],
"propertyDescriptions": {},
"tokenDescriptions": {},
"tokens": [
"handle-color",
"border-radius",
],
},
],
}
`;

exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] = `
{
"slots": [
Expand Down Expand Up @@ -246,6 +288,7 @@ exports[`Style API docs list of components with a Style API doc matches the snap
"dropdown",
"link",
"radio-button",
"slider",
"toggle",
]
`;
4 changes: 2 additions & 2 deletions src/internal/styles/style-api.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ $_focus-ring-carriers: toolkit.resolve($focus-ring, carrier);
}
}

@mixin docs($name, $map) {
@mixin docs($name, $tokens: null, $properties: null, $descriptions: null) {
@if enabled() {
@include toolkit.docs($name, $map);
@include toolkit.docs($name, $tokens, $properties, $descriptions);
}
}

Expand Down
60 changes: 60 additions & 0 deletions src/slider/__tests__/style-api.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';

import Slider from '../../../lib/components/slider';
import createWrapper from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/slider/styles.css.js';

function renderSlider(element: React.ReactElement) {
const { container } = render(element);
return { container, wrapper: createWrapper(container).findSlider()! };
}

const slots = { root: 'e', track: 'a', range: 'b', handle: 'c', unknown: 'd' };

describe('Slider Style API v2 (implicit styleClassNames)', () => {
test('applies styleClassNames to the root, track, range, and handle slots', () => {
const { container, wrapper } = renderSlider(
<Slider value={40} min={0} max={100} onChange={() => {}} {...({ styleClassNames: slots } as any)} />
);

expect(wrapper.getElement()).toHaveClass('e');
expect(container.querySelector(`.${styles['slider-track']}`)).toHaveClass('a');
expect(container.querySelector(`.${styles['slider-range']}`)).toHaveClass('b');
expect(container.querySelector(`.${styles.thumb}`)).toHaveClass('c');
expect(container.querySelector('.d')).toBeNull();
});

test('applies the range slot to filled tick marks and the track slot to the remaining ones', () => {
const { container } = renderSlider(
<Slider
value={100}
min={0}
max={100}
step={50}
tickMarks={true}
onChange={() => {}}
{...({ styleClassNames: slots } as any)}
/>
);

const ticks = Array.from(container.querySelectorAll(`.${styles.tick}`));
expect(ticks.length).toBeGreaterThan(0);
expect(
ticks.filter(tick => tick.classList.contains(styles.filled)).every(tick => tick.classList.contains('b'))
).toBe(true);
expect(
ticks.filter(tick => !tick.classList.contains(styles.filled)).every(tick => tick.classList.contains('a'))
).toBe(true);
});

test('does not leak the styleClassNames prop to the DOM', () => {
const { container } = renderSlider(
<Slider value={40} min={0} max={100} onChange={() => {}} {...({ styleClassNames: slots } as any)} />
);
expect(container.querySelector('[styleClassNames]')).toBeNull();
});
});
3 changes: 2 additions & 1 deletion src/slider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React from 'react';
import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { SliderProps } from './interfaces';
import InternalSlider from './internal';
import InternalSlider, { StyleClassNames } from './internal';

export { SliderProps };

Expand All @@ -19,6 +19,7 @@ export default function Slider({ tickMarks, hideFillLine, style, ...props }: Sli
tickMarks={tickMarks}
hideFillLine={hideFillLine}
style={style}
styleClassNames={(props as { styleClassNames?: StyleClassNames }).styleClassNames}
{...props}
{...baseComponentProps}
/>
Expand Down
19 changes: 15 additions & 4 deletions src/slider/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ import {

import styles from './styles.css.js';

// Style API v2
export interface StyleClassNames {
root?: string;
track?: string;
range?: string;
handle?: string;
}

interface InternalSliderProps extends SliderProps, InternalBaseComponentProps {
styleClassNames?: StyleClassNames;
style?: SliderProps['style'];
}

Expand All @@ -47,6 +56,7 @@ export default function InternalSlider({
valueFormatter,
i18nStrings,
style,
styleClassNames,
__internalRootRef,
...rest
}: InternalSliderProps) {
Expand Down Expand Up @@ -132,7 +142,7 @@ export default function InternalSlider({
<div
{...baseProps}
ref={__internalRootRef}
className={clsx(baseProps.className, styles.root)}
className={clsx(baseProps.className, styles.root, styleClassNames?.root)}
style={getSliderStyles(style)}
>
<div
Expand Down Expand Up @@ -161,15 +171,15 @@ export default function InternalSlider({
/>
<div className={styles.slider}>
<div
className={clsx(styles['slider-track'], {
className={clsx(styles['slider-track'], styleClassNames?.track, {
[styles.disabled]: disabled,
[styles.readonly]: readOnly,
})}
/>

{!hideFillLine && (
<div
className={clsx(styles['slider-range'], {
className={clsx(styles['slider-range'], styleClassNames?.range, {
[styles.error]: invalid,
[styles.warning]: showWarning,
[styles.active]: isActive,
Expand All @@ -184,6 +194,7 @@ export default function InternalSlider({
</div>
{!!step && tickMarks && (
<SliderTickMarks
styleClassNames={styleClassNames}
hideFillLine={hideFillLine}
disabled={disabled}
readOnly={readOnly}
Expand Down Expand Up @@ -241,7 +252,7 @@ export default function InternalSlider({
}
fireNonCancelableEvent(onChange, { value: Number(event.target.value) });
}}
className={clsx(styles.thumb, {
className={clsx(styles.thumb, styleClassNames?.handle, {
[styles.error]: invalid,
[styles.warning]: showWarning,
[styles.disabled]: disabled,
Expand Down
Loading
Loading