UNPKG

chrome-devtools-frontend

Version:
492 lines (423 loc) • 20.4 kB
// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as Root from '../root/root.js'; import * as UI from '../ui/ui.js'; import {ContrastInfo, Events as ContrastInfoEvents} from './ContrastInfo.js'; // eslint-disable-line no-unused-vars export const UIStrings = { /** *@description Label for when no contrast information is available in the color picker */ noContrastInformationAvailable: 'No contrast information available', /** *@description Text of a DOM element in Contrast Details of the Color Picker */ contrastRatio: 'Contrast ratio', /** *@description Text to show more content */ showMore: 'Show more', /** *@description Choose bg color text content in Contrast Details of the Color Picker */ pickBackgroundColor: 'Pick background color', /** *@description Tooltip text that appears when hovering over largeicon eyedropper button in Contrast Details of the Color Picker */ toggleBackgroundColorPicker: 'Toggle background color picker', /** *@description Text of a button in Contrast Details of the Color Picker *@example {rgba(0 0 0 / 100%) } PH1 */ useSuggestedColorStoFixLow: 'Use suggested color {PH1}to fix low contrast', /** *@description Label for the APCA contrast in Color Picker */ apca: 'APCA', /** *@description Label aa text content in Contrast Details of the Color Picker */ aa: 'AA', /** *@description Text that starts with a colon and includes a placeholder *@example {3.0} PH1 */ placeholderWithColon: ': {PH1}', /** *@description Label aaa text content in Contrast Details of the Color Picker */ aaa: 'AAA', /** *@description Text to show less content */ showLess: 'Show less', }; const str_ = i18n.i18n.registerUIStrings('color_picker/ContrastDetails.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class ContrastDetails extends Common.ObjectWrapper.ObjectWrapper { _contrastInfo: ContrastInfo; _element: HTMLElement; _toggleMainColorPicker: (arg0?: boolean|undefined, arg1?: Common.EventTarget.EventTargetEvent|undefined) => void; _expandedChangedCallback: () => void; _colorSelectedCallback: (arg0: Common.Color.Color) => void; _expanded: boolean; _passesAA: boolean; _contrastUnknown: boolean; _visible: boolean; _noContrastInfoAvailable: Element; _contrastValueBubble: HTMLElement; _contrastValue: HTMLElement; _contrastValueBubbleIcons: Node[]; _expandButton: UI.Toolbar.ToolbarButton; _expandedDetails: HTMLElement; _contrastThresholds: HTMLElement; _contrastAA: HTMLElement; _contrastPassFailAA: HTMLElement; _contrastAAA: HTMLElement; _contrastPassFailAAA: HTMLElement; _contrastAPCA: HTMLElement; _contrastPassFailAPCA: HTMLElement; _chooseBgColor: HTMLElement; _bgColorPickerButton: UI.Toolbar.ToolbarToggle; _bgColorPickedBound: (event: Common.EventTarget.EventTargetEvent) => void; _bgColorSwatch: Swatch; constructor( contrastInfo: ContrastInfo, contentElement: Element, toggleMainColorPickerCallback: (arg0?: boolean|undefined, arg1?: Common.EventTarget.EventTargetEvent|undefined) => void, expandedChangedCallback: () => void, colorSelectedCallback: (arg0: Common.Color.Color) => void) { super(); this._contrastInfo = contrastInfo; this._element = contentElement.createChild('div', 'spectrum-contrast-details collapsed') as HTMLElement; this._toggleMainColorPicker = toggleMainColorPickerCallback; this._expandedChangedCallback = expandedChangedCallback; this._colorSelectedCallback = colorSelectedCallback; this._expanded = false; this._passesAA = true; this._contrastUnknown = false; // This will not be visible if we don't get ContrastInfo, // e.g. for a non-font color property such as border-color. this._visible = false; // No contrast info message is created to show if it's not possible to provide the extended details. this._noContrastInfoAvailable = contentElement.createChild('div', 'no-contrast-info-available'); this._noContrastInfoAvailable.textContent = i18nString(UIStrings.noContrastInformationAvailable); this._noContrastInfoAvailable.classList.add('hidden'); const contrastValueRow = this._element.createChild('div'); contrastValueRow.addEventListener('click', this._topRowClicked.bind(this)); const contrastValueRowContents = contrastValueRow.createChild('div', 'container'); UI.UIUtils.createTextChild(contrastValueRowContents, i18nString(UIStrings.contrastRatio)); this._contrastValueBubble = contrastValueRowContents.createChild('span', 'contrast-details-value'); this._contrastValue = this._contrastValueBubble.createChild('span'); this._contrastValueBubbleIcons = []; this._contrastValueBubbleIcons.push( this._contrastValueBubble.appendChild(UI.Icon.Icon.create('smallicon-checkmark-square'))); this._contrastValueBubbleIcons.push( this._contrastValueBubble.appendChild(UI.Icon.Icon.create('smallicon-checkmark-behind'))); this._contrastValueBubbleIcons.push(this._contrastValueBubble.appendChild(UI.Icon.Icon.create('smallicon-no'))); this._contrastValueBubbleIcons.forEach(button => button.addEventListener('click', (event: Event) => { ContrastDetails._showHelp(); event.consume(false); })); const expandToolbar = new UI.Toolbar.Toolbar('expand', contrastValueRowContents); this._expandButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.showMore), 'smallicon-expand-more'); this._expandButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._expandButtonClicked.bind(this)); UI.ARIAUtils.setExpanded(this._expandButton.element, false); expandToolbar.appendToolbarItem(this._expandButton); this._expandedDetails = this._element.createChild('div', 'expanded-details'); UI.ARIAUtils.setControls(this._expandButton.element, this._expandedDetails); this._contrastThresholds = this._expandedDetails.createChild('div', 'contrast-thresholds'); this._contrastAA = this._contrastThresholds.createChild('div', 'contrast-threshold'); this._contrastPassFailAA = this._contrastAA.createChild('div', 'contrast-pass-fail'); this._contrastAAA = this._contrastThresholds.createChild('div', 'contrast-threshold'); this._contrastPassFailAAA = this._contrastAAA.createChild('div', 'contrast-pass-fail'); this._contrastAPCA = this._contrastThresholds.createChild('div', 'contrast-threshold'); this._contrastPassFailAPCA = this._contrastAPCA.createChild('div', 'contrast-pass-fail'); this._chooseBgColor = this._expandedDetails.createChild('div', 'contrast-choose-bg-color'); this._chooseBgColor.textContent = i18nString(UIStrings.pickBackgroundColor); const bgColorContainer = this._expandedDetails.createChild('div', 'background-color'); const pickerToolbar = new UI.Toolbar.Toolbar('spectrum-eye-dropper', bgColorContainer); this._bgColorPickerButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.toggleBackgroundColorPicker), 'largeicon-eyedropper'); this._bgColorPickerButton.addEventListener( UI.Toolbar.ToolbarButton.Events.Click, this._toggleBackgroundColorPicker.bind(this, undefined, true)); pickerToolbar.appendToolbarItem(this._bgColorPickerButton); this._bgColorPickedBound = this._bgColorPicked.bind(this); this._bgColorSwatch = new Swatch(bgColorContainer); this._contrastInfo.addEventListener(ContrastInfoEvents.ContrastInfoUpdated, this._update.bind(this)); } _showNoContrastInfoAvailableMessage(): void { this._noContrastInfoAvailable.classList.remove('hidden'); } _hideNoContrastInfoAvailableMessage(): void { this._noContrastInfoAvailable.classList.add('hidden'); } _computeSuggestedColor(threshold: string): Common.Color.Color|null|undefined { const fgColor = this._contrastInfo.color(); const bgColor = this._contrastInfo.bgColor(); if (!fgColor || !bgColor) { return; } if (threshold === 'APCA') { const requiredContrast = this._contrastInfo.contrastRatioAPCAThreshold(); if (requiredContrast === null) { return; } // We add 1% to the min required contrast to make sure we are over the limit. return Common.Color.Color.findFgColorForContrastAPCA(fgColor, bgColor, requiredContrast + 1); } const requiredContrast = this._contrastInfo.contrastRatioThreshold(threshold); if (!requiredContrast) { return; } // We add a bit to the required contrast to make sure we are over the limit. return Common.Color.Color.findFgColorForContrast(fgColor, bgColor, requiredContrast + 0.1); } _onSuggestColor(threshold: string): void { Host.userMetrics.colorFixed(threshold); const color = this._computeSuggestedColor(threshold); if (color) { this._colorSelectedCallback(color); } } _createFixColorButton(parent: Element, suggestedColor: Common.Color.Color): HTMLElement { const button = parent.createChild('button', 'contrast-fix-button') as HTMLElement; const originalColorFormat = this._contrastInfo.colorFormat(); const colorFormat = originalColorFormat && originalColorFormat !== Common.Color.Format.Nickname && originalColorFormat !== Common.Color.Format.Original ? originalColorFormat : Common.Color.Format.HEXA; const formattedColor = suggestedColor.asString(colorFormat); const suggestedColorString = formattedColor ? formattedColor + ' ' : ''; const label = i18nString(UIStrings.useSuggestedColorStoFixLow, {PH1: suggestedColorString}); UI.ARIAUtils.setAccessibleName(button, label); UI.Tooltip.Tooltip.install(button, label); button.tabIndex = 0; button.style.backgroundColor = suggestedColorString; return button; } _update(): void { if (this._contrastInfo.isNull()) { this._showNoContrastInfoAvailableMessage(); this.setVisible(false); return; } this.setVisible(true); this._hideNoContrastInfoAvailableMessage(); const isAPCAEnabled = Root.Runtime.experiments.isEnabled('APCA'); const fgColor = this._contrastInfo.color(); const bgColor = this._contrastInfo.bgColor(); if (isAPCAEnabled) { const apcaContrastRatio = this._contrastInfo.contrastRatioAPCA(); if (apcaContrastRatio === null || !bgColor || !fgColor) { this._contrastUnknown = true; this._contrastValue.textContent = ''; this._contrastValueBubble.classList.add('contrast-unknown'); this._chooseBgColor.classList.remove('hidden'); this._contrastThresholds.classList.add('hidden'); this._showNoContrastInfoAvailableMessage(); return; } this._contrastUnknown = false; this._chooseBgColor.classList.add('hidden'); this._contrastThresholds.classList.remove('hidden'); this._contrastValueBubble.classList.remove('contrast-unknown'); this._contrastValue.textContent = `${Platform.NumberUtilities.floor(apcaContrastRatio, 2)}%`; const apcaThreshold = this._contrastInfo.contrastRatioAPCAThreshold(); const passesAPCA = apcaContrastRatio && apcaThreshold ? Math.abs(apcaContrastRatio) >= apcaThreshold : false; this._contrastPassFailAPCA.removeChildren(); const labelAPCA = this._contrastPassFailAPCA.createChild('span', 'contrast-link-label'); labelAPCA.textContent = i18nString(UIStrings.apca); if (apcaThreshold !== null) { this._contrastPassFailAPCA.createChild('span').textContent = `: ${apcaThreshold.toFixed(2)}%`; } if (passesAPCA) { this._contrastPassFailAPCA.appendChild(UI.Icon.Icon.create('smallicon-checkmark-square')); } else { this._contrastPassFailAPCA.appendChild(UI.Icon.Icon.create('smallicon-no')); const suggestedColor = this._computeSuggestedColor('APCA'); if (suggestedColor) { const fixAPCA = this._createFixColorButton(this._contrastPassFailAPCA, suggestedColor); fixAPCA.addEventListener('click', () => this._onSuggestColor('APCA')); } } labelAPCA.addEventListener('click', (_event: Event) => ContrastDetails._showHelp()); this._element.classList.toggle('contrast-fail', !passesAPCA); this._contrastValueBubble.classList.toggle('contrast-aa', passesAPCA); return; } const contrastRatio = this._contrastInfo.contrastRatio(); if (!contrastRatio || !bgColor || !fgColor) { this._contrastUnknown = true; this._contrastValue.textContent = ''; this._contrastValueBubble.classList.add('contrast-unknown'); this._chooseBgColor.classList.remove('hidden'); this._contrastThresholds.classList.add('hidden'); this._showNoContrastInfoAvailableMessage(); return; } this._contrastUnknown = false; this._chooseBgColor.classList.add('hidden'); this._contrastThresholds.classList.remove('hidden'); this._contrastValueBubble.classList.remove('contrast-unknown'); this._contrastValue.textContent = String(Platform.NumberUtilities.floor(contrastRatio, 2)); this._bgColorSwatch.setColors(fgColor, bgColor); // In greater then comparisons we can substite null with 0. const aa = this._contrastInfo.contrastRatioThreshold('aa') || 0; this._passesAA = (this._contrastInfo.contrastRatio() || 0) >= aa; this._contrastPassFailAA.removeChildren(); const labelAA = this._contrastPassFailAA.createChild('span', 'contrast-link-label'); labelAA.textContent = i18nString(UIStrings.aa); this._contrastPassFailAA.createChild('span').textContent = i18nString(UIStrings.placeholderWithColon, {PH1: aa.toFixed(1)}); if (this._passesAA) { this._contrastPassFailAA.appendChild(UI.Icon.Icon.create('smallicon-checkmark-square')); } else { this._contrastPassFailAA.appendChild(UI.Icon.Icon.create('smallicon-no')); const suggestedColor = this._computeSuggestedColor('aa'); if (suggestedColor) { const fixAA = this._createFixColorButton(this._contrastPassFailAA, suggestedColor); fixAA.addEventListener('click', () => this._onSuggestColor('aa')); } } // In greater then comparisons we can substite null with 0. const aaa = this._contrastInfo.contrastRatioThreshold('aaa') || 0; const passesAAA = (this._contrastInfo.contrastRatio() || 0) >= aaa; this._contrastPassFailAAA.removeChildren(); const labelAAA = this._contrastPassFailAAA.createChild('span', 'contrast-link-label'); labelAAA.textContent = i18nString(UIStrings.aaa); this._contrastPassFailAAA.createChild('span').textContent = i18nString(UIStrings.placeholderWithColon, {PH1: aaa.toFixed(1)}); if (passesAAA) { this._contrastPassFailAAA.appendChild(UI.Icon.Icon.create('smallicon-checkmark-square')); } else { this._contrastPassFailAAA.appendChild(UI.Icon.Icon.create('smallicon-no')); const suggestedColor = this._computeSuggestedColor('aaa'); if (suggestedColor) { const fixAAA = this._createFixColorButton(this._contrastPassFailAAA, suggestedColor); fixAAA.addEventListener('click', () => this._onSuggestColor('aaa')); } } [labelAA, labelAAA].forEach(e => e.addEventListener('click', () => ContrastDetails._showHelp())); this._element.classList.toggle('contrast-fail', !this._passesAA); this._contrastValueBubble.classList.toggle('contrast-aa', this._passesAA); this._contrastValueBubble.classList.toggle('contrast-aaa', passesAAA); } static _showHelp(): void { Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab( UI.UIUtils.addReferrerToURL('https://web.dev/color-and-contrast-accessibility/')); } setVisible(visible: boolean): void { this._visible = visible; this._element.classList.toggle('hidden', !visible); } visible(): boolean { return this._visible; } element(): HTMLElement { return this._element; } _expandButtonClicked(_event: Common.EventTarget.EventTargetEvent): void { const selection = this._contrastValueBubble.getComponentSelection(); if (selection) { selection.empty(); } this._toggleExpanded(); } _topRowClicked(event: Event): void { const selection = this._contrastValueBubble.getComponentSelection(); if (selection) { selection.empty(); } this._toggleExpanded(); event.consume(true); } _toggleExpanded(): void { this._expanded = !this._expanded; UI.ARIAUtils.setExpanded(this._expandButton.element, this._expanded); this._element.classList.toggle('collapsed', !this._expanded); if (this._expanded) { this._toggleMainColorPicker(false); this._expandButton.setGlyph('smallicon-expand-less'); this._expandButton.setTitle(i18nString(UIStrings.showLess)); if (this._contrastUnknown) { this._toggleBackgroundColorPicker(true); } } else { this._toggleBackgroundColorPicker(false); this._expandButton.setGlyph('smallicon-expand-more'); this._expandButton.setTitle(i18nString(UIStrings.showMore)); } this._expandedChangedCallback(); } collapse(): void { this._element.classList.remove('expanded'); this._toggleBackgroundColorPicker(false); this._toggleMainColorPicker(false); } expanded(): boolean { return this._expanded; } backgroundColorPickerEnabled(): boolean { return this._bgColorPickerButton.toggled(); } toggleBackgroundColorPicker(enabled: boolean): void { this._toggleBackgroundColorPicker(enabled, false); } _toggleBackgroundColorPicker(enabled?: boolean, shouldTriggerEvent: boolean|undefined = true): void { if (enabled === undefined) { enabled = !this._bgColorPickerButton.toggled(); } this._bgColorPickerButton.setToggled(enabled); if (shouldTriggerEvent) { this.dispatchEventToListeners(Events.BackgroundColorPickerWillBeToggled, enabled); } Host.InspectorFrontendHost.InspectorFrontendHostInstance.setEyeDropperActive(enabled); if (enabled) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener( Host.InspectorFrontendHostAPI.Events.EyeDropperPickedColor, this._bgColorPickedBound); } else { Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener( Host.InspectorFrontendHostAPI.Events.EyeDropperPickedColor, this._bgColorPickedBound); } } _bgColorPicked(event: Common.EventTarget.EventTargetEvent): void { const rgbColor = event.data as { r: number, g: number, b: number, a: number, }; const rgba = [rgbColor.r, rgbColor.g, rgbColor.b, (rgbColor.a / 2.55 | 0) / 100]; const color = Common.Color.Color.fromRGBA(rgba); this._contrastInfo.setBgColor(color); this._toggleBackgroundColorPicker(false); Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront(); } } export const Events = { BackgroundColorPickerWillBeToggled: Symbol('BackgroundColorPickerWillBeToggled'), }; export class Swatch { _parentElement: Element; _swatchElement: Element; _swatchInnerElement: HTMLElement; _textPreview: HTMLElement; constructor(parentElement: Element) { this._parentElement = parentElement; this._swatchElement = parentElement.createChild('span', 'swatch contrast swatch-inner-white'); this._swatchInnerElement = this._swatchElement.createChild('span', 'swatch-inner') as HTMLElement; this._textPreview = this._swatchElement.createChild('div', 'text-preview') as HTMLElement; this._textPreview.textContent = 'Aa'; } setColors(fgColor: Common.Color.Color, bgColor: Common.Color.Color): void { this._textPreview.style.color = fgColor.asString(Common.Color.Format.RGBA) as string; this._swatchInnerElement.style.backgroundColor = bgColor.asString(Common.Color.Format.RGBA) as string; // Show border if the swatch is white. this._swatchElement.classList.toggle('swatch-inner-white', bgColor.hsla()[2] > 0.9); } }