.group {
+ --constrained-content-max-width: calc(50cqw - 100cqw * var(--content-margin-fraction) * 2);
+ }
+
+ .constrainContentWidth > .group-lg {
+ --content-margin-fraction: var(--content-margin-fraction-override);
+ --content-margin: calc(var(--content-margin-fraction) * 100cqw);
+
+ max-width: min(calc(var(--section-max-width, 100%) + var(--content-margin) * 2),
+ calc(50% + var(--layout-inline-lg-content-max-width)));
+ }
+}
+
.restrict-xxs,
.restrict-xs,
.restrict-sm {
@@ -99,6 +123,18 @@
width: var(--content-width);
}
+.constrainContentWidth .side {
+ position: relative;
+ --side-offset: calc((min(var(--layout-inline-content-max-width), var(--constrained-content-max-width)) -
+ var(--content-max-width)) / 2);
+ right: var(--side-offset);
+}
+
+.constrainContentWidth.right .side {
+ right: auto;
+ left: var(--side-offset);
+}
+
.sticky {
composes: side;
position: sticky;
diff --git a/entry_types/scrolled/package/src/frontend/layouts/index.js b/entry_types/scrolled/package/src/frontend/layouts/index.js
index 2df9f09545..bedae7a117 100644
--- a/entry_types/scrolled/package/src/frontend/layouts/index.js
+++ b/entry_types/scrolled/package/src/frontend/layouts/index.js
@@ -15,7 +15,8 @@ export const Layout = React.memo(
prevProps.appearance === nextProps.appearance &&
prevProps.contentAreaRef === nextProps.contentAreaRef &&
prevProps.sectionProps === nextProps.sectionProps &&
- prevProps.isContentPadded === nextProps.isContentPadded
+ prevProps.isContentPadded === nextProps.isContentPadded &&
+ prevProps.constrainContentWidth === nextProps.constrainContentWidth
)
);
diff --git a/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js
new file mode 100644
index 0000000000..584e2f5d86
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import Fullscreen from '../Fullscreen';
+import {splitOverlayStyle} from '../splitOverlayStyle';
+import styles from './SplitShadow.module.css';
+
+export default function SplitShadow(props) {
+ if (props.motifAreaState.isContentPadded) {
+ return (
+
+ {props.children}
+
+ );
+ }
+
+ return (
+
+
+
+
+ {props.children}
+
+ );
+}
diff --git a/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.module.css b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.module.css
new file mode 100644
index 0000000000..1a48e4a674
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.module.css
@@ -0,0 +1,48 @@
+.wrapper {
+ position: relative;
+}
+
+.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ pointer-events: none;
+}
+
+@media screen and (min-width: 950px) {
+ .overlay {
+ width: 50%;
+ }
+}
+
+.align-left .overlay {
+ left: 0;
+}
+
+.align-right .overlay {
+ left: auto;
+ right: 0;
+}
+
+.align-center .overlay,
+.align-centerRagged .overlay {
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.dark .overlay {
+ background-color: rgba(0, 0, 0, 0.7);
+}
+
+.light .overlay {
+ background-color: rgba(255, 255, 255, 0.7);
+}
+
+@media print {
+ .overlay {
+ display: none;
+ }
+}
diff --git a/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js b/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js
new file mode 100644
index 0000000000..fbe318b6f2
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js
@@ -0,0 +1,25 @@
+import {isTranslucentColor} from './utils/isTranslucentColor';
+
+export function splitOverlayStyle({color, backdropBlur}) {
+ const style = {};
+
+ if (color) {
+ style.backgroundColor = color;
+ }
+
+ const blur = resolvedBackdropBlur({color, backdropBlur});
+
+ if (blur > 0) {
+ style.backdropFilter = `blur(${blur / 100 * 10}px)`;
+ }
+
+ return style;
+}
+
+function resolvedBackdropBlur({color, backdropBlur}) {
+ if (color && !isTranslucentColor(color)) {
+ return 0;
+ }
+
+ return backdropBlur ?? 100;
+}
diff --git a/entry_types/scrolled/package/src/frontend/utils/index.js b/entry_types/scrolled/package/src/frontend/utils/index.js
index 9bbd383aaf..9051e502fb 100644
--- a/entry_types/scrolled/package/src/frontend/utils/index.js
+++ b/entry_types/scrolled/package/src/frontend/utils/index.js
@@ -5,11 +5,13 @@ import {
isBlankEditableTextValue,
presence,
} from './blank';
+import {isTranslucentColor} from './isTranslucentColor';
export const utils = {
capitalize,
camelize,
isBlank,
isBlankEditableTextValue,
+ isTranslucentColor,
presence
}
diff --git a/entry_types/scrolled/package/src/frontend/utils/isTranslucentColor.js b/entry_types/scrolled/package/src/frontend/utils/isTranslucentColor.js
new file mode 100644
index 0000000000..46ff97d638
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/utils/isTranslucentColor.js
@@ -0,0 +1,3 @@
+export function isTranslucentColor(color) {
+ return !!color && color.length > 7 && color.slice(-2).toLowerCase() !== 'ff';
+}
diff --git a/entry_types/scrolled/package/values/properties.css b/entry_types/scrolled/package/values/properties.css
new file mode 100644
index 0000000000..abe516401e
--- /dev/null
+++ b/entry_types/scrolled/package/values/properties.css
@@ -0,0 +1,5 @@
+@property --constrained-content-max-width {
+ syntax: "
";
+ inherits: true;
+ initial-value: 9999px;
+}
diff --git a/package/spec/ui/views/ColorPicker-spec.js b/package/spec/ui/views/ColorPicker-spec.js
index e4af63d03b..0c7b867bbc 100644
--- a/package/spec/ui/views/ColorPicker-spec.js
+++ b/package/spec/ui/views/ColorPicker-spec.js
@@ -478,6 +478,140 @@ describe('ColorPicker', () => {
});
});
+ describe('swatch filtering', () => {
+ it('filters out translucent swatches when alpha is not enabled', () => {
+ createColorPicker({swatches: ['#aabbcc', '#ff000080', '#112233']});
+
+ var buttons = picker().querySelectorAll('.color_picker-swatches button');
+ expect(buttons).toHaveLength(2);
+ expect(buttons[0].textContent).toBe('#aabbcc');
+ expect(buttons[1].textContent).toBe('#112233');
+ });
+
+ it('keeps translucent swatches when alpha is enabled', () => {
+ createColorPicker({alpha: true, swatches: ['#aabbcc', '#ff000080', '#112233']});
+
+ var buttons = picker().querySelectorAll('.color_picker-swatches button');
+ expect(buttons).toHaveLength(3);
+ });
+
+ it('filters translucent swatches on update', () => {
+ createColorPicker({swatches: ['#aabbcc']});
+
+ colorPicker.update({swatches: ['#112233', '#ff000080']});
+
+ var buttons = picker().querySelectorAll('.color_picker-swatches button');
+ expect(buttons).toHaveLength(1);
+ expect(buttons[0].textContent).toBe('#112233');
+ });
+ });
+
+ describe('fallbackColor option', () => {
+ it('uses fallbackColor for display when opened with no value', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ open();
+
+ expect(colorPicker._displayColor).toEqual({r: 255, g: 0, b: 0, a: 1});
+ });
+
+ it('does not set fallbackColor as currentColor', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ open();
+
+ expect(colorPicker._currentColor).toBeNull();
+ });
+
+ it('does not write fallbackColor to input', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ open();
+
+ expect(input.value).toBe('');
+ });
+
+ it('sets wrapper color to fallbackColor when input is cleared', () => {
+ createColorPicker({value: '#00ff00', fallbackColor: '#ff0000'});
+
+ input.value = '';
+ input.dispatchEvent(new Event('input', {bubbles: true}));
+
+ expect(input.parentNode).toHaveStyle('color: #ff0000');
+ });
+
+ it('sets wrapper color to updated fallbackColor', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ colorPicker.update({fallbackColor: '#00ff00'});
+
+ expect(input.parentNode).toHaveStyle('color: #00ff00');
+ });
+
+ it('uses updated fallbackColor', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ colorPicker.update({fallbackColor: '#00ff00'});
+ open();
+
+ expect(colorPicker._displayColor).toEqual({r: 0, g: 255, b: 0, a: 1});
+ });
+
+ it('sets wrapper color to fallbackColor when no value is set', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ expect(input.parentNode).toHaveStyle('color: #ff0000');
+ });
+
+ it('sets aria-description on input when displaying fallback', () => {
+ createColorPicker({
+ fallbackColor: '#ff0000',
+ fallbackColorDescription: 'Automatic color'
+ });
+
+ expect(input).toHaveAccessibleDescription('Automatic color: #ff0000');
+ });
+
+ it('does not set aria-description without fallbackColorDescription', () => {
+ createColorPicker({fallbackColor: '#ff0000'});
+
+ expect(input).not.toHaveAccessibleDescription();
+ });
+
+ it('clears aria-description when color is set', () => {
+ createColorPicker({
+ fallbackColor: '#ff0000',
+ fallbackColorDescription: 'Automatic color',
+ swatches: ['#00ff00']
+ });
+ open();
+
+ picker().querySelector('.color_picker-swatches button')
+ .dispatchEvent(new Event('click', {bubbles: true}));
+
+ expect(input).not.toHaveAccessibleDescription();
+ });
+
+ it('updates aria-description when fallbackColor changes', () => {
+ createColorPicker({
+ fallbackColor: '#ff0000',
+ fallbackColorDescription: 'Automatic color'
+ });
+
+ colorPicker.update({fallbackColor: '#00ff00'});
+
+ expect(input).toHaveAccessibleDescription('Automatic color: #00ff00');
+ });
+
+ it('prefers defaultColor over fallbackColor', () => {
+ createColorPicker({defaultValue: '#0000ff', fallbackColor: '#ff0000'});
+
+ open();
+
+ expect(colorPicker._displayColor).toEqual({r: 0, g: 0, b: 255, a: 1});
+ });
+ });
+
describe('destroy', () => {
it('removes picker element from DOM', () => {
createColorPicker();
diff --git a/package/spec/ui/views/inputs/ColorInputView-spec.js b/package/spec/ui/views/inputs/ColorInputView-spec.js
index a2cac9650a..36962b138c 100644
--- a/package/spec/ui/views/inputs/ColorInputView-spec.js
+++ b/package/spec/ui/views/inputs/ColorInputView-spec.js
@@ -1,376 +1,245 @@
import Backbone from 'backbone';
-import sinon from 'sinon';
import '@testing-library/jest-dom/extend-expect';
+import userEvent from '@testing-library/user-event';
import {ColorInputView} from 'pageflow/ui';
-
-import {ColorInput} from '$support/dominos/ui'
+import {renderBackboneView} from 'testHelpers/renderBackboneView';
describe('pageflow.ColorInputView', () => {
- let testContext;
+ const user = userEvent.setup({delay: null});
- beforeEach(() => {
- testContext = {};
- });
+ function render(options) {
+ return renderBackboneView(new ColorInputView({
+ disableChangeDebounce: true,
+ ...options
+ }));
+ }
- beforeEach(() => {
- testContext.clock = sinon.useFakeTimers();
- });
-
- afterEach(() => {
- testContext.clock.restore();
- });
+ async function fillIn(input, value) {
+ await user.clear(input);
+ await user.type(input, value);
+ }
it('loads value into input', () => {
- var model = new Backbone.Model({
- color: '#ababab'
- });
- var colorInputView = new ColorInputView({
- model: model,
+ const {getByRole} = render({
+ model: new Backbone.Model({color: '#ababab'}),
propertyName: 'color'
});
- var colorInput = ColorInput.render(colorInputView);
-
- expect(colorInput.value()).toBe('#ababab');
+ expect(getByRole('textbox')).toHaveValue('#ababab');
});
it('updates input when model changes', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color'
- });
+ const model = new Backbone.Model();
+ const {getByRole} = render({model, propertyName: 'color'});
- var colorInput = ColorInput.render(colorInputView);
model.set('color', '#ababab');
- expect(colorInput.value()).toBe('#ababab');
+ expect(getByRole('textbox')).toHaveValue('#ababab');
});
- it('saves value to model on change', () => {
- var model = new Backbone.Model({
- color: '#ababab'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color'
- });
+ it('saves value to model on change', async () => {
+ const model = new Backbone.Model({color: '#ababab'});
+ const {getByRole} = render({model, propertyName: 'color'});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#bbb', testContext.clock);
+ await fillIn(getByRole('textbox'), '#bbb');
expect(model.get('color')).toBe('#bbbbbb');
});
- it('does not update color input with normalized value', () => {
- var model = new Backbone.Model({
- color: '#ababab'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color'
- });
+ it('does not update color input with normalized value', async () => {
+ const model = new Backbone.Model({color: '#ababab'});
+ const {getByRole} = render({model, propertyName: 'color'});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#bbb', testContext.clock);
+ await fillIn(getByRole('textbox'), '#bbb');
- expect(colorInput.value()).toBe('#bbb');
+ expect(getByRole('textbox')).toHaveValue('#bbb');
});
it('allows passing swatches', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getAllByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
swatches: ['#cdcdcd', '#dedede']
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.swatches()).toEqual(['rgb(205, 205, 205)', 'rgb(222, 222, 222)']);
+ const buttons = getAllByRole('button');
+ expect(buttons).toHaveLength(2);
+ expect(buttons[0]).toHaveTextContent('#cdcdcd');
+ expect(buttons[1]).toHaveTextContent('#dedede');
});
describe('with defaultValue option', () => {
it('falls back to default value', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
defaultValue: '#cdcdcd'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.value()).toBe('#cdcdcd');
+ expect(getByRole('textbox')).toHaveValue('#cdcdcd');
});
- it('does not store default value in model', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: '#cdcdcd'
- });
+ it('does not store default value in model', async () => {
+ const model = new Backbone.Model();
+ const {getByRole} = render({model, propertyName: 'color', defaultValue: '#cdcdcd'});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.has('color')).toBe(false);
});
- it('stores non default value in model', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: '#cdcdcd'
- });
+ it('stores non default value in model', async () => {
+ const model = new Backbone.Model();
+ const {getByRole} = render({model, propertyName: 'color', defaultValue: '#cdcdcd'});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#ababab', testContext.clock);
+ await fillIn(getByRole('textbox'), '#ababab');
expect(model.get('color')).toBe('#ababab');
});
- it('unsets attribute in model if choosing default value', () => {
- var model = new Backbone.Model({
- color: '#fff'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: '#cdcdcd'
- });
+ it('unsets attribute in model if choosing default value', async () => {
+ const model = new Backbone.Model({color: '#fff'});
+ const {getByRole} = render({model, propertyName: 'color', defaultValue: '#cdcdcd'});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.has('color')).toBe(false);
});
it('includes swatch for default value', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getAllByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
defaultValue: '#cdcdcd',
swatches: ['#dedede']
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.swatches()).toEqual(['rgb(205, 205, 205)', 'rgb(222, 222, 222)']);
+ const buttons = getAllByRole('button');
+ expect(buttons).toHaveLength(2);
+ expect(buttons[0]).toHaveTextContent('#cdcdcd');
+ expect(buttons[1]).toHaveTextContent('#dedede');
});
it('does not duplicate swatch', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getAllByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
defaultValue: '#cdcdcd',
swatches: ['#dedede', '#cdcdcd']
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.swatches()).toEqual(['rgb(205, 205, 205)', 'rgb(222, 222, 222)']);
+ expect(getAllByRole('button')).toHaveLength(2);
});
});
describe('with function as defaultValue option', () => {
it('falls back to default value', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
- defaultValue: function() { return '#cdcdcd'; }
+ defaultValue: () => '#cdcdcd'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.value()).toBe('#cdcdcd');
+ expect(getByRole('textbox')).toHaveValue('#cdcdcd');
});
- it('does not store default value in model', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: function() { return '#cdcdcd'; }
+ it('does not store default value in model', async () => {
+ const model = new Backbone.Model();
+ const {getByRole} = render({
+ model, propertyName: 'color',
+ defaultValue: () => '#cdcdcd'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.has('color')).toBe(false);
});
- it('stores non default value in model', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: function() { return '#cdcdcd'; }
+ it('stores non default value in model', async () => {
+ const model = new Backbone.Model();
+ const {getByRole} = render({
+ model, propertyName: 'color',
+ defaultValue: () => '#cdcdcd'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#ababab', testContext.clock);
+ await fillIn(getByRole('textbox'), '#ababab');
expect(model.get('color')).toBe('#ababab');
});
- it('unsets attribute in model if choosing default value', () => {
- var model = new Backbone.Model({
- color: '#fff'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValue: function() { return '#cdcdcd'; }
+ it('unsets attribute in model if choosing default value', async () => {
+ const model = new Backbone.Model({color: '#fff'});
+ const {getByRole} = render({
+ model, propertyName: 'color',
+ defaultValue: () => '#cdcdcd'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.has('color')).toBe(false);
});
it('includes swatch for default value', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model: model,
+ const {getAllByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
- defaultValue: function() { return '#cdcdcd'; },
+ defaultValue: () => '#cdcdcd',
swatches: ['#dedede']
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.swatches()).toEqual(['rgb(205, 205, 205)', 'rgb(222, 222, 222)']);
+ const buttons = getAllByRole('button');
+ expect(buttons).toHaveLength(2);
+ expect(buttons[0]).toHaveTextContent('#cdcdcd');
});
});
describe('with defaultValueBinding option', () => {
it('uses value of binding attribute as default value', () => {
- var model = new Backbone.Model({
- default_color: '#cdcdcd'
- });
- var colorInputView = new ColorInputView({
- model: model,
+ const {getByRole} = render({
+ model: new Backbone.Model({default_color: '#cdcdcd'}),
propertyName: 'color',
defaultValueBinding: 'default_color'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
+ expect(getByRole('textbox')).toHaveValue('#cdcdcd');
+ });
+
+ it('updates displayed default value when binding attribute changes', () => {
+ const model = new Backbone.Model({default_color: '#aaaaaa'});
+ const {getByRole} = render({model, propertyName: 'color', defaultValueBinding: 'default_color'});
+
+ model.set('default_color', '#cdcdcd');
- expect(colorInput.value()).toBe('#cdcdcd');
+ expect(getByRole('textbox')).toHaveValue('#cdcdcd');
});
- it(
- 'updates displayed default value when binding attribute changes',
- () => {
- var model = new Backbone.Model({
- default_color: '#aaaaaa'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValueBinding: 'default_color'
- });
-
- var colorInput = ColorInput.render(
- colorInputView
- );
- model.set('default_color', '#cdcdcd');
-
- expect(colorInput.value()).toBe('#cdcdcd');
- }
- );
-
- it('does not store default value in model', () => {
- var model = new Backbone.Model({
- default_color: '#cdcdcd'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValueBinding: 'default_color'
- });
+ it('does not store default value in model', async () => {
+ const model = new Backbone.Model({default_color: '#cdcdcd'});
+ const {getByRole} = render({model, propertyName: 'color', defaultValueBinding: 'default_color'});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('default_color', '#aaaaaa');
- colorInput.fillIn('#aaaaaa', testContext.clock);
+ await fillIn(getByRole('textbox'), '#aaaaaa');
expect(model.has('color')).toBe(false);
});
- it('stores non default value in model', () => {
- var model = new Backbone.Model({
- default_color: '#cdcdcd'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValueBinding: 'default_color'
- });
+ it('stores non default value in model', async () => {
+ const model = new Backbone.Model({default_color: '#cdcdcd'});
+ const {getByRole} = render({model, propertyName: 'color', defaultValueBinding: 'default_color'});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('default_color', '#aaaaaa');
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.get('color')).toBe('#cdcdcd');
});
- it('unsets attribute in model if choosing default value', () => {
- var model = new Backbone.Model({
- color: '#fff'
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValueBinding: 'default_color'
- });
+ it('unsets attribute in model if choosing default value', async () => {
+ const model = new Backbone.Model({color: '#fff'});
+ const {getByRole} = render({model, propertyName: 'color', defaultValueBinding: 'default_color'});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('default_color', '#cdcdcd');
- colorInput.fillIn('#cdcdcd', testContext.clock);
+ await fillIn(getByRole('textbox'), '#cdcdcd');
expect(model.has('color')).toBe(false);
});
@@ -378,171 +247,130 @@ describe('pageflow.ColorInputView', () => {
describe('with function as defaultValue and defaultValueBinding option', () => {
it('passes binding attribute to default value function', () => {
- var model = new Backbone.Model({
- light: true
- });
- var colorInputView = new ColorInputView({
- model: model,
+ const {getByRole} = render({
+ model: new Backbone.Model({light: true}),
propertyName: 'color',
defaultValueBinding: 'light',
- defaultValue: function(light) { return light ? '#fefefe' : '#010101'; }
+ defaultValue: light => light ? '#fefefe' : '#010101'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.value()).toBe('#fefefe');
+ expect(getByRole('textbox')).toHaveValue('#fefefe');
});
- it(
- 'updates displayed default value when binding attribute changes',
- () => {
- var model = new Backbone.Model({
- light: true
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
- defaultValueBinding: 'light',
- defaultValue: function(light) { return light ? '#fefefe' : '#010101'; }
- });
-
- var colorInput = ColorInput.render(
- colorInputView
- );
- model.set('light', false);
-
- expect(colorInput.value()).toBe('#010101');
- }
- );
-
- it('does not store default value in model', () => {
- var model = new Backbone.Model({
- light: true
+ it('updates displayed default value when binding attribute changes', () => {
+ const model = new Backbone.Model({light: true});
+ const {getByRole} = render({
+ model, propertyName: 'color',
+ defaultValueBinding: 'light',
+ defaultValue: light => light ? '#fefefe' : '#010101'
});
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
+
+ model.set('light', false);
+
+ expect(getByRole('textbox')).toHaveValue('#010101');
+ });
+
+ it('does not store default value in model', async () => {
+ const model = new Backbone.Model({light: true});
+ const {getByRole} = render({
+ model, propertyName: 'color',
defaultValueBinding: 'light',
- defaultValue: function(light) { return light ? '#fefefe' : '#010101'; }
+ defaultValue: light => light ? '#fefefe' : '#010101'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('light', false);
- colorInput.fillIn('#010101', testContext.clock);
+ await fillIn(getByRole('textbox'), '#010101');
expect(model.has('color')).toBe(false);
});
- it('stores non default value in model', () => {
- var model = new Backbone.Model({
- light: true
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
+ it('stores non default value in model', async () => {
+ const model = new Backbone.Model({light: true});
+ const {getByRole} = render({
+ model, propertyName: 'color',
defaultValueBinding: 'light',
- defaultValue: function(light) { return light ? '#fefefe' : '#010101'; }
+ defaultValue: light => light ? '#fefefe' : '#010101'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('light', false);
- colorInput.fillIn('#fefefe', testContext.clock);
+ await fillIn(getByRole('textbox'), '#fefefe');
expect(model.get('color')).toBe('#fefefe');
});
- it('unsets attribute in model if choosing default value', () => {
- var model = new Backbone.Model({
- light: true
- });
- var colorInputView = new ColorInputView({
- model: model,
- propertyName: 'color',
+ it('unsets attribute in model if choosing default value', async () => {
+ const model = new Backbone.Model({light: true});
+ const {getByRole} = render({
+ model, propertyName: 'color',
defaultValueBinding: 'light',
- defaultValue: function(light) { return light ? '#fefefe' : '#010101'; }
+ defaultValue: light => light ? '#fefefe' : '#010101'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
model.set('light', false);
- colorInput.fillIn('#010101', testContext.clock);
+ await fillIn(getByRole('textbox'), '#010101');
expect(model.has('color')).toBe(false);
});
});
it('removes picker element when view is closed', () => {
- var pickersBefore = document.querySelectorAll('.color_picker').length;
- var colorInputView = new ColorInputView({
+ const pickersBefore = document.querySelectorAll('.color_picker').length;
+ const view = new ColorInputView({
model: new Backbone.Model(),
propertyName: 'color'
});
- ColorInput.render(colorInputView);
+ renderBackboneView(view);
expect(document.querySelectorAll('.color_picker').length).toBe(pickersBefore + 1);
- colorInputView.close();
+ view.close();
expect(document.querySelectorAll('.color_picker').length).toBe(pickersBefore);
});
describe('with placeholderColor option', () => {
- it('sets custom property', () => {
- var colorInputView = new ColorInputView({
+ it('sets fallback color', () => {
+ const {getByRole} = render({
model: new Backbone.Model(),
propertyName: 'color',
- placeholderColor: '#fff'
+ placeholderColor: '#ff0000',
+ placeholderColorDescription: 'Automatic color'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #fff');
+ expect(getByRole('textbox'))
+ .toHaveAccessibleDescription('Automatic color: #ff0000');
});
});
- describe('with function as placeholderColor and placeholderColorBinding option', () => {
- it('sets custom property', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
- model,
+ describe('with placeholderColor and placeholderColorBinding option', () => {
+ it('sets fallback color', () => {
+ const {getByRole} = render({
+ model: new Backbone.Model(),
propertyName: 'color',
placeholderColorBinding: 'invert',
- placeholderColor: invert => invert ? '#000' : '#fff'
+ placeholderColor: invert => invert ? '#000000' : '#ffffff',
+ placeholderColorDescription: 'Automatic color'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
- expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #fff');
+ expect(getByRole('textbox'))
+ .toHaveAccessibleDescription('Automatic color: #ffffff');
});
- it('updates custom property', () => {
- var model = new Backbone.Model();
- var colorInputView = new ColorInputView({
+ it('updates fallback color', () => {
+ const model = new Backbone.Model();
+ const {getByRole} = render({
model,
propertyName: 'color',
placeholderColorBinding: 'invert',
- placeholderColor: invert => invert ? '#000' : '#fff'
+ placeholderColor: invert => invert ? '#000000' : '#ffffff',
+ placeholderColorDescription: 'Automatic color'
});
- var colorInput = ColorInput.render(
- colorInputView
- );
-
model.set('invert', true);
- expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #000');
+ expect(getByRole('textbox'))
+ .toHaveAccessibleDescription('Automatic color: #000000');
});
});
});
diff --git a/package/src/testHelpers/dominos/ui/index.js b/package/src/testHelpers/dominos/ui/index.js
index 7f7a18c933..546d5b1de4 100644
--- a/package/src/testHelpers/dominos/ui/index.js
+++ b/package/src/testHelpers/dominos/ui/index.js
@@ -2,6 +2,5 @@ export * from './ConfigurationEditor'
export * from './ConfigurationEditorTab'
export * from './Table'
export * from './Tabs';
-export * from './inputs/ColorInput'
export * from './inputs/RadioButtonGroupInput'
export * from './inputs/SelectInput'
diff --git a/package/src/testHelpers/dominos/ui/inputs/ColorInput.js b/package/src/testHelpers/dominos/ui/inputs/ColorInput.js
deleted file mode 100644
index 80cd1859e4..0000000000
--- a/package/src/testHelpers/dominos/ui/inputs/ColorInput.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import {Base} from '../../Base';
-
-export const ColorInput = Base.extend({
- value: function() {
- return this._input().val();
- },
-
- fillIn: function(value, clock) {
- var input = this._input()[0];
- input.value = value;
- input.dispatchEvent(new Event('input', {bubbles: true}));
-
- clock.tick(500);
- },
-
- swatches: function() {
- var picker = this._input()[0].parentNode.querySelector('.color_picker');
- var buttons = picker.querySelectorAll('.color_picker-swatches button');
-
- return Array.from(buttons).map(function(button) {
- return window.getComputedStyle(button).color;
- });
- },
-
- _input: function() {
- return this.$el.find('input');
- }
-});
diff --git a/package/src/ui/views/ColorPicker.js b/package/src/ui/views/ColorPicker.js
index 7aa0913f19..e7563a13f0 100644
--- a/package/src/ui/views/ColorPicker.js
+++ b/package/src/ui/views/ColorPicker.js
@@ -3,7 +3,8 @@
const ctx = typeof OffscreenCanvas !== 'undefined' &&
new OffscreenCanvas(1, 1).getContext('2d');
-const FALLBACK_COLOR = {r: 255, g: 255, b: 255, a: 1};
+const DEFAULT_DISPLAY_COLOR = {r: 255, g: 255, b: 255, a: 1};
+let nextDescriptionId = 0;
const PICKER_HTML =
'' +
@@ -27,6 +28,8 @@ export default class ColorPicker {
this._alpha = options.alpha || false;
this._onChange = options.onChange;
this._defaultColor = strToRGBA(options.defaultValue);
+ this._fallbackColor = strToRGBA(options.fallbackColor);
+ this._fallbackColorDescription = options.fallbackColorDescription;
this._swatches = options.swatches || [];
this._wrapInput();
@@ -36,10 +39,19 @@ export default class ColorPicker {
this._updateColor(strToRGBA(this._input.value), {silent: true});
}
+ setValue(str) {
+ this._input.value = str || '';
+ this._updateColor(strToRGBA(str), {silent: true});
+ }
+
update(options) {
if ('defaultValue' in options) {
this._defaultColor = strToRGBA(options.defaultValue);
}
+ if ('fallbackColor' in options) {
+ this._fallbackColor = strToRGBA(options.fallbackColor);
+ this._updateColor(this._currentColor, {silent: true});
+ }
if (options.swatches) {
this._swatches = options.swatches;
this._renderSwatches();
@@ -80,6 +92,14 @@ export default class ColorPicker {
parent.insertBefore(wrapper, this._input);
wrapper.className = 'color_picker-field';
wrapper.appendChild(this._input);
+
+ if (this._fallbackColorDescription) {
+ this._descriptionElement = document.createElement('span');
+ this._descriptionElement.hidden = true;
+ this._descriptionElement.id = 'color_picker_desc_' + nextDescriptionId++;
+ wrapper.appendChild(this._descriptionElement);
+ this._input.setAttribute('aria-describedby', this._descriptionElement.id);
+ }
}
}
@@ -94,14 +114,18 @@ export default class ColorPicker {
}
_renderSwatches() {
+ const swatches = this._alpha
+ ? this._swatches
+ : this._swatches.filter(s => !isTranslucentSwatch(s));
+
this._swatchesContainer.textContent = '';
- this._swatchesContainer.classList.toggle('color_picker-empty', !this._swatches.length);
+ this._swatchesContainer.classList.toggle('color_picker-empty', !swatches.length);
- if (!this._swatches.length) {
+ if (!swatches.length) {
return;
}
- this._swatches.forEach(swatch => {
+ swatches.forEach(swatch => {
const button = document.createElement('button');
button.setAttribute('type', 'button');
button.title = swatch;
@@ -255,7 +279,7 @@ export default class ColorPicker {
if (rgba && !this._alpha) rgba.a = 1;
this._currentColor = rgba && {...this._displayColor, ...rgba};
- this._displayColor = this._currentColor || FALLBACK_COLOR;
+ this._displayColor = this._currentColor || this._fallbackColor || DEFAULT_DISPLAY_COLOR;
const hex = rgbaToHex(this._displayColor);
const opaqueHex = hex.substring(0, 7);
@@ -265,9 +289,15 @@ export default class ColorPicker {
this._alphaMarker.style.color = hex;
const formatted = this._formatHex(this._currentColor);
+ const fallbackHex = this._formatHex(this._fallbackColor);
const wrapper = this._input.parentNode;
if (wrapper && wrapper.classList.contains('color_picker-field')) {
- wrapper.style.color = formatted || '';
+ wrapper.style.color = formatted || fallbackHex || '';
+ }
+
+ if (this._descriptionElement) {
+ this._descriptionElement.textContent =
+ !formatted && fallbackHex ? `${this._fallbackColorDescription}: ${fallbackHex}` : '';
}
// Force repaint the color and alpha gradients (Chrome workaround)
@@ -608,3 +638,7 @@ function getClipRect(element) {
return viewport;
}
+
+function isTranslucentSwatch(str) {
+ return str.length > 7 && str.slice(-2).toLowerCase() !== 'ff';
+}
diff --git a/package/src/ui/views/inputs/ColorInputView.js b/package/src/ui/views/inputs/ColorInputView.js
index 09ed2f6727..1f0d54adb8 100644
--- a/package/src/ui/views/inputs/ColorInputView.js
+++ b/package/src/ui/views/inputs/ColorInputView.js
@@ -65,8 +65,10 @@ export const ColorInputView = Marionette.ItemView.extend({
this._colorPicker = new ColorPicker(this.ui.input[0], {
alpha: this.options.alpha,
defaultValue: this.defaultValue(),
+ fallbackColor: this.getAttributeBoundOption('placeholderColor'),
+ fallbackColorDescription: this.options.placeholderColorDescription,
swatches: this.getSwatches(),
- onChange: _.debounce(_.bind(this._onChange, this), 200)
+ onChange: this._debouncedOnChange()
});
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
@@ -79,11 +81,8 @@ export const ColorInputView = Marionette.ItemView.extend({
},
updatePlaceholderColor(value) {
- if (value) {
- this.el.style.setProperty('--placeholder-color', value);
- }
- else {
- this.el.style.removeProperty('--placeholder-color');
+ if (this._colorPicker) {
+ this._colorPicker.update({fallbackColor: value});
}
},
@@ -100,12 +99,7 @@ export const ColorInputView = Marionette.ItemView.extend({
var color = this.model.get(this.options.propertyName) || this.defaultValue() || '';
if (!this._saving) {
- this.ui.input[0].value = color;
-
- var wrapper = this.ui.input[0].parentNode;
- if (wrapper && wrapper.classList.contains('color_picker-field')) {
- wrapper.style.color = color;
- }
+ this._colorPicker.setValue(color);
}
this.$el.toggleClass('is_default', !this.model.has(this.options.propertyName));
@@ -141,6 +135,11 @@ export const ColorInputView = Marionette.ItemView.extend({
}
},
+ _debouncedOnChange: function() {
+ const handler = _.bind(this._onChange, this);
+ return this.options.disableChangeDebounce ? handler : _.debounce(handler, 200);
+ },
+
_onChange: function(color) {
this._saving = true;