From f7cb02876a82b8d8aff3c7ecc7cc1493fefb115a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 3 Dec 2021 17:58:19 +0100 Subject: [PATCH 1/9] show tooltips on hover in eventtile Signed-off-by: Kerry Archibald --- src/components/views/elements/Tooltip.tsx | 3 + .../views/elements/TooltipButton.tsx | 30 +++------ .../views/elements/TooltipTarget.tsx | 61 +++++++++++++++++++ src/components/views/rooms/EventTile.tsx | 2 +- 4 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 src/components/views/elements/TooltipTarget.tsx diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index c335684c057..fb3333bc883 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -46,6 +46,9 @@ interface IProps { label: React.ReactNode; alignment?: Alignment; // defaults to Natural yOffset?: number; + // id describing tooltip + // used to associate tooltip with target for a11y + id?: string; } @replaceableComponent("views.elements.Tooltip") diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 26e46c7da86..342131a0fbe 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import Tooltip from './Tooltip'; +import { TooltipTarget } from './TooltipTarget'; interface IProps { helpText: React.ReactNode | string; @@ -36,29 +36,17 @@ export default class TooltipButton extends React.Component { }; } - private onMouseOver = () => { - this.setState({ - hover: true, - }); - }; - - private onMouseLeave = () => { - this.setState({ - hover: false, - }); - }; - render() { - const tip = this.state.hover ? :
; return ( -
+ ? - { tip } -
+ ); } } diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx new file mode 100644 index 00000000000..422e14a3bd6 --- /dev/null +++ b/src/components/views/elements/TooltipTarget.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2017 New Vector Ltd. +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState } from 'react'; +import Tooltip from './Tooltip'; + +interface IProps { + tooltip: React.ReactNode | string; + className?: string; + tooltipClassName?: string; + tooltipContainerClassName?: string; + id: string; +} + +/** + * Generic tooltip target element that handles tooltip visibility state + * and displays children + */ +export const TooltipTarget: React.FC = ({ + className, id, children, tooltip, tooltipClassName, tooltipContainerClassName, +}) => { + const [isVisible, setIsVisible] = useState(false); + + const show = () => setIsVisible(true); + const hide = () => setIsVisible(false); + + return ( +
+ { children } + +
+ ); +}; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 8cf59a2d5d0..73b24be6bc8 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1202,7 +1202,7 @@ export default class EventTile extends React.Component { _t( 'Re-request encryption keys from your other sessions.', {}, - { 'requestLink': (sub) => { sub } }, + { 'requestLink': (sub) => { sub } }, ); const keyRequestInfo = isEncryptionFailure && !isRedacted ? From d6b17541cb1d415315f10b7a4d7e4f24ea3408e1 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 6 Dec 2021 10:53:11 +0100 Subject: [PATCH 2/9] use tooltip props pass thru --- src/components/views/elements/Tooltip.tsx | 4 +- .../views/elements/TooltipButton.tsx | 7 ++-- .../views/elements/TooltipTarget.tsx | 40 +++++++++++-------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index fb3333bc883..199e107ca57 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -33,7 +33,7 @@ export enum Alignment { Bottom, // Centered } -interface IProps { +export interface ITooltipProps { // Class applied to the element used to position the tooltip className?: string; // Class applied to the tooltip itself @@ -52,7 +52,7 @@ interface IProps { } @replaceableComponent("views.elements.Tooltip") -export default class Tooltip extends React.Component { +export default class Tooltip extends React.Component { private tooltipContainer: HTMLElement; private tooltip: void | Element | Component; private parent: Element; diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 342131a0fbe..4cca5e05b3c 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -39,11 +39,10 @@ export default class TooltipButton extends React.Component { render() { return ( ? diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx index 422e14a3bd6..1fda8df9171 100644 --- a/src/components/views/elements/TooltipTarget.tsx +++ b/src/components/views/elements/TooltipTarget.tsx @@ -15,15 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from 'react'; -import Tooltip from './Tooltip'; - -interface IProps { - tooltip: React.ReactNode | string; - className?: string; - tooltipClassName?: string; - tooltipContainerClassName?: string; - id: string; +import React, { useState, HTMLAttributes } from 'react'; +import Tooltip, { ITooltipProps } from './Tooltip'; + +interface IProps extends HTMLAttributes, Omit { + tooltipTargetClassName?: string; } /** @@ -31,7 +27,16 @@ interface IProps { * and displays children */ export const TooltipTarget: React.FC = ({ - className, id, children, tooltip, tooltipClassName, tooltipContainerClassName, + children, + tooltipTargetClassName, + // tooltip pass through props + className, + id, + label, + alignment, + yOffset, + tooltipClassName, + ...rest }) => { const [isVisible, setIsVisible] = useState(false); @@ -39,23 +44,26 @@ export const TooltipTarget: React.FC = ({ const hide = () => setIsVisible(false); return ( -
{ children } -
+ ); }; From 6fa556169c37529b927fc38c432abb419ad93caa Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 6 Dec 2021 11:07:49 +0100 Subject: [PATCH 3/9] use tooltiptarget in InfoTooltip Signed-off-by: Kerry Archibald --- src/components/views/elements/InfoTooltip.tsx | 42 ++++--------------- .../views/elements/TooltipButton.tsx | 9 +--- .../views/elements/TooltipTarget.tsx | 4 +- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 123e1189655..1ca0eac77e3 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -18,9 +18,10 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; -import Tooltip, { Alignment } from './Tooltip'; +import { Alignment } from './Tooltip'; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { TooltipTarget } from './TooltipTarget'; export enum InfoTooltipKind { Info = "info", @@ -34,29 +35,10 @@ interface ITooltipProps { kind?: InfoTooltipKind; } -interface IState { - hover: boolean; -} - @replaceableComponent("views.elements.InfoTooltip") export default class InfoTooltip extends React.PureComponent { constructor(props: ITooltipProps) { super(props); - this.state = { - hover: false, - }; - } - - onMouseOver = () => { - this.setState({ - hover: true, - }); - }; - - onMouseLeave = () => { - this.setState({ - hover: false, - }); }; render() { @@ -68,22 +50,16 @@ export default class InfoTooltip extends React.PureComponent :
; return ( -
- { children } - { tip } -
+ {children} + ); } } diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 4cca5e05b3c..52477695163 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -23,17 +23,10 @@ interface IProps { helpText: React.ReactNode | string; } -interface IState { - hover: boolean; -} - @replaceableComponent("views.elements.TooltipButton") -export default class TooltipButton extends React.Component { +export default class TooltipButton extends React.Component { constructor(props) { super(props); - this.state = { - hover: false, - }; } render() { diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx index 1fda8df9171..b37f4162a80 100644 --- a/src/components/views/elements/TooltipTarget.tsx +++ b/src/components/views/elements/TooltipTarget.tsx @@ -44,7 +44,7 @@ export const TooltipTarget: React.FC = ({ const hide = () => setIsVisible(false); return ( - = ({ alignment={alignment} visible={isVisible} /> - +
); }; From b6f59d183c76c514d8eb17d54d6b39fd7c196a00 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 6 Dec 2021 12:00:41 +0100 Subject: [PATCH 4/9] use target in TestWithTooltip Signed-off-by: Kerry Archibald --- res/css/views/elements/_TextWithTooltip.scss | 3 ++ .../elements/AccessibleTooltipButton.tsx | 2 + src/components/views/elements/InfoTooltip.tsx | 4 +- .../views/elements/TextWithTooltip.tsx | 39 +++++++------------ .../views/elements/TooltipTarget.tsx | 2 +- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/res/css/views/elements/_TextWithTooltip.scss b/res/css/views/elements/_TextWithTooltip.scss index a7f9cb74830..4a3702d6c16 100644 --- a/res/css/views/elements/_TextWithTooltip.scss +++ b/res/css/views/elements/_TextWithTooltip.scss @@ -13,6 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +.mx_TextWithTooltip_target { + display: inline; +} .mx_TextWithTooltip_tooltip { display: none; diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index e239028ab81..252ab9e726b 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -80,6 +80,8 @@ export default class AccessibleTooltipButton extends React.PureComponent { children } diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 1ca0eac77e3..6ce7cd21506 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -39,7 +39,7 @@ interface ITooltipProps { export default class InfoTooltip extends React.PureComponent { constructor(props: ITooltipProps) { super(props); - }; + } render() { const { tooltip, children, tooltipClassName, className, kind } = this.props; @@ -58,7 +58,7 @@ export default class InfoTooltip extends React.PureComponent - {children} + { children } ); } diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index b7c24771588..121da9349bf 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -15,8 +15,9 @@ */ import React from 'react'; +import classNames from 'classnames'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import Tooltip from "./Tooltip"; +import { TooltipTarget } from './TooltipTarget'; interface IProps { class?: string; @@ -26,41 +27,27 @@ interface IProps { onClick?: (ev?: React.MouseEvent) => void; } -interface IState { - hover: boolean; -} - @replaceableComponent("views.elements.TextWithTooltip") -export default class TextWithTooltip extends React.Component { +export default class TextWithTooltip extends React.Component { constructor(props: IProps) { super(props); - - this.state = { - hover: false, - }; } - private onMouseOver = (): void => { - this.setState({ hover: true }); - }; - - private onMouseLeave = (): void => { - this.setState({ hover: false }); - }; - public render(): JSX.Element { const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; return ( - + { children } - { this.state.hover && } - + ); } } diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx index b37f4162a80..30720bac702 100644 --- a/src/components/views/elements/TooltipTarget.tsx +++ b/src/components/views/elements/TooltipTarget.tsx @@ -52,7 +52,7 @@ export const TooltipTarget: React.FC = ({ onMouseLeave={hide} onFocus={show} onBlur={hide} - { ...rest } + {...rest} > { children } Date: Mon, 6 Dec 2021 12:07:56 +0100 Subject: [PATCH 5/9] tsc fixes Signed-off-by: Kerry Archibald --- src/components/views/elements/ActionButton.tsx | 14 ++++++++++---- src/components/views/elements/InfoTooltip.tsx | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ActionButton.tsx b/src/components/views/elements/ActionButton.tsx index 390e84be777..40ec50160c4 100644 --- a/src/components/views/elements/ActionButton.tsx +++ b/src/components/views/elements/ActionButton.tsx @@ -58,15 +58,19 @@ export default class ActionButton extends React.Component { }; private onMouseEnter = (): void => { - if (this.props.tooltip) this.setState({ showTooltip: true }); + this.onShowTooltip(); if (this.props.mouseOverAction) { dis.dispatch({ action: this.props.mouseOverAction }); } }; - private onMouseLeave = (): void => { + private onShowTooltip = (): void => { + if (this.props.tooltip) this.setState({ showTooltip: true }); + } + + private onHideTooltip = (): void => { this.setState({ showTooltip: false }); - }; + } render() { let tooltip; @@ -88,7 +92,9 @@ export default class ActionButton extends React.Component { className={classNames.join(" ")} onClick={this.onClick} onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} + onMouseLeave={this.onHideTooltip} + onFocus={this.onShowTooltip} + onBlur={this.onHideTooltip} aria-label={this.props.label} > { icon } diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 6ce7cd21506..a323ce035d0 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -36,7 +36,7 @@ interface ITooltipProps { } @replaceableComponent("views.elements.InfoTooltip") -export default class InfoTooltip extends React.PureComponent { +export default class InfoTooltip extends React.PureComponent { constructor(props: ITooltipProps) { super(props); } From d2ad24fa87052dcf80c219914ad4097a005cc4c5 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 6 Dec 2021 12:54:28 +0100 Subject: [PATCH 6/9] test tooltip target Signed-off-by: Kerry Archibald --- .../views/elements/TooltipTarget-test.tsx | 90 +++++++++++++++++++ .../__snapshots__/TooltipTarget-test.tsx.snap | 29 ++++++ 2 files changed, 119 insertions(+) create mode 100644 test/components/views/elements/TooltipTarget-test.tsx create mode 100644 test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap diff --git a/test/components/views/elements/TooltipTarget-test.tsx b/test/components/views/elements/TooltipTarget-test.tsx new file mode 100644 index 00000000000..fe318e38ec9 --- /dev/null +++ b/test/components/views/elements/TooltipTarget-test.tsx @@ -0,0 +1,90 @@ +// skinned-sdk should be the first import in most tests +import '../../../skinned-sdk'; +import React from "react"; +import { + renderIntoDocument, + Simulate, +} from 'react-dom/test-utils'; +import { act } from "react-dom/test-utils"; + +import { Alignment } from '../../../../src/components/views/elements/Tooltip'; +import { TooltipTarget } from "../../../../src/components/views/elements/TooltipTarget"; + +describe('', () => { + const defaultProps = { + "tooltipTargetClassName": 'test tooltipTargetClassName', + "className": 'test className', + "tooltipClassName": 'test tooltipClassName', + "label": 'test label', + "yOffset": 1, + "alignment": Alignment.Left, + "id": 'test id', + 'data-test-id': 'test', + }; + + const getComponent = (props = {}) => { + const wrapper = renderIntoDocument( + // wrap in element so renderIntoDocument can render functional component + + + child + + , + ) as HTMLSpanElement; + return wrapper.querySelector('[data-test-id=test]'); + }; + + const getVisibleTooltip = () => document.querySelector('.mx_Tooltip.mx_Tooltip_visible'); + + afterEach(() => { + // clean up visible tooltips + const tooltipWrapper = document.querySelector('.mx_Tooltip_wrapper'); + document.body.removeChild(tooltipWrapper); + }); + + it('renders container', () => { + const component = getComponent(); + expect(component).toMatchSnapshot(); + expect(getVisibleTooltip()).toBeFalsy(); + }); + + it('displays tooltip on mouseover', () => { + const wrapper = getComponent(); + act(() => { + Simulate.mouseOver(wrapper); + }); + expect(getVisibleTooltip()).toMatchSnapshot(); + }); + + it('hides tooltip on mouseleave', () => { + const wrapper = getComponent(); + act(() => { + Simulate.mouseOver(wrapper); + }); + expect(getVisibleTooltip()).toBeTruthy(); + act(() => { + Simulate.mouseLeave(wrapper); + }); + expect(getVisibleTooltip()).toBeFalsy(); + }); + + it('displays tooltip on focus', () => { + const wrapper = getComponent(); + act(() => { + Simulate.focus(wrapper); + }); + expect(getVisibleTooltip()).toBeTruthy(); + }); + + it('hides tooltip on blur', async () => { + const wrapper = getComponent(); + act(() => { + Simulate.focus(wrapper); + }); + expect(getVisibleTooltip()).toBeTruthy(); + await act(async () => { + await Simulate.blur(wrapper); + }); + expect(getVisibleTooltip()).toBeFalsy(); + }); +}); diff --git a/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap new file mode 100644 index 00000000000..cdb12e5af41 --- /dev/null +++ b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` displays tooltip on mouseover 1`] = ` +
+
+ test label +
+`; + +exports[` renders container 1`] = ` +
+ + child + +
+
+`; From 6f22382d4e4eefb986426c735e5ba02c100cf0fc Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 6 Dec 2021 14:37:35 +0100 Subject: [PATCH 7/9] lint fix Signed-off-by: Kerry Archibald --- src/components/views/elements/ActionButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ActionButton.tsx b/src/components/views/elements/ActionButton.tsx index 40ec50160c4..2a2929a3452 100644 --- a/src/components/views/elements/ActionButton.tsx +++ b/src/components/views/elements/ActionButton.tsx @@ -66,11 +66,11 @@ export default class ActionButton extends React.Component { private onShowTooltip = (): void => { if (this.props.tooltip) this.setState({ showTooltip: true }); - } + }; private onHideTooltip = (): void => { this.setState({ showTooltip: false }); - } + }; render() { let tooltip; From 26b936ff9a6d0541b3bce96da29cd704a9159ec5 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 7 Dec 2021 18:12:11 +0100 Subject: [PATCH 8/9] rename tooltip handlers Signed-off-by: Kerry Archibald --- .../views/elements/AccessibleTooltipButton.tsx | 12 ++++++------ src/components/views/elements/ActionButton.tsx | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 252ab9e726b..7f40662efe4 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -52,14 +52,14 @@ export default class AccessibleTooltipButton extends React.PureComponent { + showTooltip = () => { if (this.props.forceHide) return; this.setState({ hover: true, }); }; - onMouseLeave = () => { + hideTooltip = () => { this.setState({ hover: false, }); @@ -78,10 +78,10 @@ export default class AccessibleTooltipButton extends React.PureComponent { children } diff --git a/src/components/views/elements/ActionButton.tsx b/src/components/views/elements/ActionButton.tsx index 2a2929a3452..178aca8ca92 100644 --- a/src/components/views/elements/ActionButton.tsx +++ b/src/components/views/elements/ActionButton.tsx @@ -58,17 +58,17 @@ export default class ActionButton extends React.Component { }; private onMouseEnter = (): void => { - this.onShowTooltip(); + this.showTooltip(); if (this.props.mouseOverAction) { dis.dispatch({ action: this.props.mouseOverAction }); } }; - private onShowTooltip = (): void => { + private showTooltip = (): void => { if (this.props.tooltip) this.setState({ showTooltip: true }); }; - private onHideTooltip = (): void => { + private hideTooltip = (): void => { this.setState({ showTooltip: false }); }; @@ -92,9 +92,9 @@ export default class ActionButton extends React.Component { className={classNames.join(" ")} onClick={this.onClick} onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onHideTooltip} - onFocus={this.onShowTooltip} - onBlur={this.onHideTooltip} + onMouseLeave={this.hideTooltip} + onFocus={this.showTooltip} + onBlur={this.hideTooltip} aria-label={this.props.label} > { icon } From a87e20792c5fc454a8f3a22f4f926e5041befa90 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 7 Dec 2021 18:12:57 +0100 Subject: [PATCH 9/9] update copyright to 2021 Signed-off-by: Kerry Archibald --- src/components/views/elements/TooltipTarget.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx index 30720bac702..88ce02b92ce 100644 --- a/src/components/views/elements/TooltipTarget.tsx +++ b/src/components/views/elements/TooltipTarget.tsx @@ -1,6 +1,5 @@ /* -Copyright 2017 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.