Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add withTextMagnifier mixin for enabling magnified text on focus #544

Closed
wants to merge 11 commits into from
2 changes: 2 additions & 0 deletions packages/@lightningjs/ui-components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export { default as TitleRow } from './src/components/TitleRow/TitleRow';
export { default as Toggle } from './src/components/Toggle/Toggle';
export { default as ToggleSmall } from './src/components/Toggle/ToggleSmall';
export { default as Tooltip } from './src/components/Tooltip/Tooltip';
export { default as TextMagnifier } from './src/components/TextMagnifier/TextMagnifier';

// Globals
export { default as context } from './src/globals/context';
Expand All @@ -102,6 +103,7 @@ export {
generateAbbrevConfig,
defaultAbbrevConfig
} from './src/mixins/withAnnouncer';
export { default as withTextMagnifier } from './src/mixins/withTextMagnifier';
export { default as Speech } from './src/mixins/withAnnouncer/Speech';
export { default as withClassCache } from './src/mixins/withClassCache';
export { default as withHandleKey } from './src/mixins/withHandleKey';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default class Surface extends Base {
return this.w;
}

get _radius() {
return getMaxRoundRadius(this.style.radius, this.w, this.h);
}

_update() {
this._updateLayout();
this._updateScale();
Expand All @@ -62,7 +66,7 @@ export default class Surface extends Base {
texture: lng.Tools.getRoundRect(
this.innerW - 2, // Reference the underscored values here in cause the h or w getters need to be overwritten for alignment - see Tile
this.innerH - 2,
getMaxRoundRadius(this.style.radius, this.w, this.h),
this._radius,
0,
null,
true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Surface, { SurfaceStyle } from '../Surface';
import { StylePartial } from '../../types/lui';

type TextMagnifierStyle = SurfaceStyle & {
color: string;
gutterX: number;
gutterY: number;
h: number;
joshhowenstine marked this conversation as resolved.
Show resolved Hide resolved
marginX: number;
joshhowenstine marked this conversation as resolved.
Show resolved Hide resolved
radius: number | string;
textStyle: any;

Check warning on line 11 in packages/@lightningjs/ui-components/src/components/TextMagnifier/TextMagnifier.d.ts

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Unexpected any. Specify a different type

Check warning on line 11 in packages/@lightningjs/ui-components/src/components/TextMagnifier/TextMagnifier.d.ts

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Unexpected any. Specify a different type

Check warning on line 11 in packages/@lightningjs/ui-components/src/components/TextMagnifier/TextMagnifier.d.ts

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Unexpected any. Specify a different type

Check warning on line 11 in packages/@lightningjs/ui-components/src/components/TextMagnifier/TextMagnifier.d.ts

View workflow job for this annotation

GitHub Actions / quality / lint-unit

Unexpected any. Specify a different type
joshhowenstine marked this conversation as resolved.
Show resolved Hide resolved
};

export default class TextMagnifier extends Surface {
get style(): TextMagnifierStyle;
set style(v: StylePartial<TextMagnifierStyle>);
location: 'top' | 'bottom';
mode: string;
content: string | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright 2023 Comcast Cable Communications Management, LLC
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

import Surface from '../Surface/Surface';
import ScrollWrapper from '../ScrollWrapper/ScrollWrapper.js';
import * as styles from './TextMagnifier.styles.js';

export default class TextMagnifier extends Surface {
static get __componentName() {
return 'TextMagnifier';
}

static get __themeStyle() {
return styles;
}

static get properties() {
return ['location'];
}

get mode() {
return 'unfocused';
}

set mode(_) {
// Mode is disabled, no action needed
}

get content() {
return this._content;
}

set content(value) {
if (this._content !== value) {
this._content = value;
this._updateScrollWrapper();
}
}

get _totalHeight() {
const baseHeight =
this.style.textStyle.lineHeight + Math.max(...this._radius) * 2; // Accommodate for all configurations of radii
return Math.max(baseHeight, this.style.textStyle.lineHeight * 2); // Ensure the fade clears
}

get _radius() {
return this.location === 'top'
? [0, 0, this.style.radius, this.style.radius]
: [this.style.radius, this.style.radius, 0, 0];
}

_construct() {
super._construct();
this._location = 'top';
this._content = null;
}

static _template() {
return {
...super._template(),
ScrollWrapper: {
type: ScrollWrapper,
autoScroll: true
}
};
}

_update() {
const renderPrecision = this.stage.getRenderPrecision();
const stageWidth = this.stage.w / renderPrecision;

this.patch({
w: stageWidth - this.style.gutterX * 2,
h: this._totalHeight,
x: this.style.gutterX,
mountY: this.location === 'top' ? 0 : 1,
y: this.location === 'top' ? 0 : this.stage.h / renderPrecision,
zIndex: this.style.zIndex
});

this._updateScrollWrapper();
super._update();
}

_updateScrollWrapper() {
const yCenter = this._totalHeight / 2;
const yOffset = yCenter - this.style.textStyle.fontSize / 2;
const adjustedHeight = this._totalHeight - yOffset;
const radius = Math.max(...this._radius);
const gutterX = this.style.gutterX;
const patchWidth = this.w - Math.max(radius, gutterX) * 2;
const patchX = Math.max(radius, gutterX);

this.tag('ScrollWrapper').patch({
alpha: this._content && this._content.length ? 1 : 0,
content: this._content,
h: adjustedHeight,
w: patchWidth,
x: patchX,
y: yOffset,
style: {
textStyle: this.style.textStyle,
fadeHeight: this.style.textStyle.lineHeight,
contentMarginTop: 0,
contentMarginLeft: 0
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright 2023 Comcast Cable Communications Management, LLC
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

import lng from '@lightningjs/core';
import { default as TextMagnifierComponent } from '.';
import { context } from '@lightningjs/ui-components/src';

export default {
title: 'Components/TextMagnifier'
};

export const TextMagnifier = () =>
joshhowenstine marked this conversation as resolved.
Show resolved Hide resolved
class TextMagnifier extends lng.Component {
static _template() {
return {
TextMagnifier: {
type: TextMagnifierComponent,
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec aliquet fermentum massa, nec scelerisque magna sodales viverra. Fusce mauris neque, aliquam vitae hendrerit vitae, tempor eu nunc. Vestibulum ligula tellus, feugiat facilisis diam non, scelerisque eleifend justo. Sed fermentum dui velit, eu imperdiet enim pretium vitae. Sed molestie, ex nec lacinia ornare, turpis urna egestas justo, sit amet blandit sapien turpis egestas dui. Sed vel pharetra elit. Quisque auctor risus a elit posuere fermentum. Integer interdum enim vitae arcu faucibus vulputate. Pellentesque sed dolor quis erat venenatis scelerisque in faucibus lectus. Mauris ac semper ante, at lobortis nunc.'
}
};
}

_init() {
this.parent.x = -context.theme.layout.marginX;
this.parent.y = -context.theme.layout.marginY;

super._init();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright 2023 Comcast Cable Communications Management, LLC
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

export const base = theme => {
return {
color: theme.color.fillNeutral,
gutterX: theme.layout.gutterX * 2,
gutterY: theme.layout.safe,
radius: theme.radius.lg,
textStyle: theme.typography.display1,
zIndex: 9999
};
};

export const tone = theme => ({
neutral: {
backgroundColor: theme.color.fillNeutral,
textStyle: { textColor: theme.color.textInverse }
},
inverse: {
backgroundColor: theme.color.fillInverse,
textStyle: { textColor: theme.color.textNeutral }
},
brand: {
backgroundColor: theme.color.fillBrand,
textStyle: { textColor: theme.color.textNeutral }
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright 2023 Comcast Cable Communications Management, LLC
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/

export { default as default } from './TextMagnifier';
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export { default as withSelections } from './withSelections';
export { default as withTags } from './withTags';
export { default as withThemeStyles } from './withThemeStyles';
export { default as withUpdates } from './withUpdates';
export { default as withTextMagnifier } from './withTextMagnifier';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import lng from '@lightningjs/core';

export interface WithTextMagnifier {
set textMagnifierEnabled(val: boolean);
get textMagnifierEnabled(): boolean;
}

export interface WithTextMagnifierConstructor {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (...args: any[]): WithTextMagnifier;
}

export default function withAnnouncer<T extends typeof lng.Component>(
base: T
): T & WithAnnouncerConstructor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import lng from '@lightningjs/core';
import TextMagnifier from '../../components/TextMagnifier/TextMagnifier';

function isSubclass(Subclass, Superclass) {
// Traverse up the prototype chain to check if the Superclass is found
while (Subclass) {
if (Subclass === Superclass) {
return true;
}
Subclass = Object.getPrototypeOf(Subclass);
}
return false;
}

export default function (Base) {
if (!isSubclass(Base, lng.Application)) {
console.error(
'withTextMagnifier can only be applied to an application class'
);
return Base;
}

return class extends Base {
set textMagnifierEnabled(val) {
this._textMagnifierEnabled = val;
this._updateTextMagnifier(true);
}

get textMagnifierEnabled() {
return this._textMagnifierEnabled;
}

get _focusedElement() {
return this.application?.focusPath?.[
this.application.focusPath.length - 1
];
}

get _location() {
const { py: focusedY } = this._focusedElement?.core?.renderContext || {};
return focusedY > this.stage.h / 2 ? 'top' : 'bottom';
}

get _focusText() {
const { title, description, announce } = this._focusedElement || {};
joshhowenstine marked this conversation as resolved.
Show resolved Hide resolved
const focusText = title || description || announce || '';
return Array.isArray(focusText)
? focusText.filter(Boolean).join('. ').replace(/\.$/, '')
: focusText.replace(/\.$/, '');
}

_focusChange() {
this._updateTextMagnifier();
super._focusChange();
}

_updateTextMagnifier() {
if (
!this.textMagnifierEnabled ||
!this._focusedElement ||
!this._focusText.length
) {
if (this.tag('TextMagnifier')) {
this.patch({ TextMagnifier: undefined });
}
return;
}

this.patch({
TextMagnifier: {
type: TextMagnifier,
location: this._location,
content: this._focusText
}
});
}
};
}
Loading
Loading