Skip to content

Commit 41e5cad

Browse files
vogtnnmetulev
andauthored
accessibility: localization & rtl (#619)
* localization service and helper * mgt-person rtl styles * mgt-login strings and rtl * adding localization service to components * adding directionality to mgt-person * case for no replacement string provided * people-picker localization and rtl * additional people-picker strings * additional filtering on strings for localization in case of punctutation * teams channel picker localization and rtl * removing issue with strings * updating .rtl selector * mgt-flyout position * mgt tasks+todo associated subcomponents rtl * mgt-agenda ltr localize * updated with common enums for easier selection of localized strings * new method removing localization-service and building through LocalizationHelper * localization new method per component * agenda * tasks and task base * new person card localization * delete tasks * cancel task * update name * updating localization methods to use full string group * new localization for mgt-login * new method of providing strings per component * basic per component implementation * restore person-card * cleanup * localizationHelper * new structure * better string names * logged out rtl css * todo * comments * leftover code * no changes needed * updating getstrings method refactor/remove servestrings * base component * strings getter for todo * leftovers * updating directionality method * removing importants * update direction into baseComponent and mutationObserver updates * cleanup * adding rtl storybook story * css necessary to overwrite ltr styles * teams-channel-picker * fixing tasks and todo rtl + strings * scope issue mutation observer fires event now * dot-options and task css updates * localization addon for storybook and fix for flyout * removing old dropdown * handling for no strings and RTL only for accessible stories * localization stories handling * adding reactTypes * extra import cleanup * removing old theme logic * removed * updating input variable name to match theming * moving localization story to general, improving addon with newStrings Co-authored-by: Nikola Metulev <[email protected]>
1 parent 30a36a0 commit 41e5cad

File tree

31 files changed

+689
-48
lines changed

31 files changed

+689
-48
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import addons, { makeDecorator } from '@storybook/addons';
2+
import { LocalizationHelper } from '../../../packages/mgt-element/dist/utils/LocalizationHelper';
3+
import { withKnobs, text, object, select } from '@storybook/addon-knobs';
4+
import { STORY_CHANGED, STORY_INIT, FORCE_RE_RENDER } from '@storybook/core-events';
5+
6+
export const localize = makeDecorator({
7+
name: `localize`,
8+
parameterName: 'myParameter',
9+
decorator: [withKnobs],
10+
skipIfNoParametersOrOptions: false,
11+
wrapper: (getStory, context, { parameters }) => {
12+
LocalizationHelper.strings = context.parameters.strings;
13+
14+
let defaultStrings = {
15+
_components: {
16+
login: {
17+
signInLinkSubtitle: 'Sign In',
18+
signOutLinkSubtitle: 'Sign Out'
19+
},
20+
'people-picker': {
21+
inputPlaceholderText: 'Start typing a name',
22+
noResultsFound: `We didn't find any matches.`,
23+
loadingMessage: 'Loading...'
24+
},
25+
'teams-channel-picker': {
26+
inputPlaceholderText: 'Select a channel',
27+
noResultsFound: `We didn't find any matches.`,
28+
loadingMessage: 'Loading...'
29+
},
30+
tasks: {
31+
removeTaskSubtitle: 'Delete Task',
32+
cancelNewTaskSubtitle: 'cancel',
33+
newTaskPlaceholder: 'Task...',
34+
addTaskButtonSubtitle: 'Add'
35+
},
36+
todo: {
37+
removeTaskSubtitle: 'Delete Task',
38+
cancelNewTaskSubtitle: 'cancel',
39+
newTaskPlaceholder: 'Task...',
40+
addTaskButtonSubtitle: 'Add'
41+
}
42+
}
43+
};
44+
45+
for (let component in defaultStrings['_components']) {
46+
for (let stringKey in defaultStrings['_components'][component]) {
47+
let keys = stringKey;
48+
if (LocalizationHelper.strings['_components'][component][keys]) {
49+
for (let key in LocalizationHelper.strings['_components'][component]) {
50+
let value = text(key, LocalizationHelper.strings['_components'][component][key], component);
51+
if (value) {
52+
LocalizationHelper.strings['_components'][component][key] = value;
53+
}
54+
}
55+
}
56+
}
57+
}
58+
59+
let channel = addons.getChannel();
60+
channel.on(STORY_CHANGED, () => {
61+
LocalizationHelper.strings = defaultStrings;
62+
});
63+
64+
object('DefaultStrings', { ...defaultStrings }, 'DefaultStrings');
65+
if (context.name === 'Localization') {
66+
LocalizationHelper.strings = context.parameters.strings;
67+
}
68+
object('NewStrings', { ...context.parameters.strings }, 'NewStrings');
69+
70+
return getStory(context);
71+
}
72+
});

.storybook/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ module.exports = {
1818
],
1919
stories: ['../stories/**/*.stories.(js|mdx)'],
2020
addons: [
21-
// '@storybook/addon-a11y/register',
22-
// '@storybook/addon-actions/register',
23-
// '@storybook/addon-knobs/register',
24-
// '@storybook/addon-links/register'
21+
// '@storybook/addon-a11y/register',
22+
// '@storybook/addon-actions/register',
23+
'@storybook/addon-knobs/register',
24+
// '@storybook/addon-links/register'
2525
],
2626
webpackFinal: async (config, { configType }) => {
2727
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'

packages/mgt-element/src/components/baseComponent.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* -------------------------------------------------------------------------------------------
66
*/
77

8-
import { LitElement, PropertyValues } from 'lit-element';
8+
import { internalProperty, LitElement, PropertyValues } from 'lit-element';
99
import { Providers } from '../providers/Providers';
10+
import { LocalizationHelper } from '../utils/LocalizationHelper';
1011

1112
/**
1213
* Defines media query based on component width
@@ -40,6 +41,8 @@ export enum ComponentMediaQuery {
4041
* @extends {LitElement}
4142
*/
4243
export abstract class MgtBaseComponent extends LitElement {
44+
@internalProperty() public direction = 'ltr';
45+
4346
/**
4447
* Get ShadowRoot toggle, returns value of _useShadowRoot
4548
*
@@ -101,6 +104,17 @@ export abstract class MgtBaseComponent extends LitElement {
101104

102105
private static _useShadowRoot: boolean = true;
103106

107+
/**
108+
* returns component strings
109+
*
110+
* @readonly
111+
* @protected
112+
* @memberof MgtBaseComponent
113+
*/
114+
protected get strings(): { [x: string]: string } {
115+
return {};
116+
}
117+
104118
/**
105119
* determines if login component is in loading state
106120
* @type {boolean}
@@ -115,6 +129,37 @@ export abstract class MgtBaseComponent extends LitElement {
115129
if (this.isShadowRootDisabled()) {
116130
(this as any)._needsShimAdoptedStyleSheets = true;
117131
}
132+
this.handleLocalizationChanged = this.handleLocalizationChanged.bind(this);
133+
this.updateDirection = this.updateDirection.bind(this);
134+
this.updateDirection();
135+
}
136+
137+
/**
138+
* Invoked each time the custom element is appended into a document-connected element
139+
*
140+
* @memberof MgtBaseComponent
141+
*/
142+
public connectedCallback() {
143+
super.connectedCallback();
144+
LocalizationHelper.onStringsUpdated(this.handleLocalizationChanged);
145+
LocalizationHelper.onDirectionUpdated(this.updateDirection);
146+
}
147+
148+
public disconnectedCallback() {
149+
super.disconnectedCallback();
150+
LocalizationHelper.removeOnStringsUpdated(this.handleLocalizationChanged);
151+
LocalizationHelper.removeOnDirectionUpdated(this.updateDirection);
152+
}
153+
154+
/**
155+
* Request localization changes when the 'strings' event is detected
156+
*
157+
* @protected
158+
* @memberof MgtBaseComponent
159+
*/
160+
protected handleLocalizationChanged() {
161+
this.requestUpdate();
162+
LocalizationHelper.updateStringsForTag(this.tagName, this.strings);
118163
}
119164

120165
/**
@@ -249,4 +294,14 @@ export abstract class MgtBaseComponent extends LitElement {
249294
this._isLoadingState = value;
250295
this.requestUpdate('isLoadingState');
251296
}
297+
298+
/**
299+
* Adds rtl attribute to component if direction is returned rtl
300+
*
301+
* @private
302+
* @memberof MgtBaseComponent
303+
*/
304+
protected updateDirection() {
305+
this.direction = LocalizationHelper.getDocumentDirection();
306+
}
252307
}

packages/mgt-element/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './utils/EventDispatcher';
1515
export * from './utils/TemplateContext';
1616
export * from './utils/TemplateHelper';
1717
export * from './utils/equals';
18+
export * from './utils/LocalizationHelper';
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* -------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4+
* See License in the project root for license information.
5+
* -------------------------------------------------------------------------------------------
6+
*/
7+
8+
import { EventDispatcher, EventHandler } from './EventDispatcher';
9+
10+
/**
11+
* Helper class for Localization
12+
*
13+
*
14+
* @export
15+
* @class LocalizationHelper
16+
*/
17+
export class LocalizationHelper {
18+
static _strings: any;
19+
20+
static _stringsEventDispatcher: EventDispatcher<any> = new EventDispatcher();
21+
22+
static _directionEventDispatcher: EventDispatcher<any> = new EventDispatcher();
23+
24+
private static mutationObserver;
25+
26+
public static get strings() {
27+
return this._strings;
28+
}
29+
30+
/**
31+
* Set strings to be localized
32+
*
33+
* @static
34+
* @memberof LocalizationHelper
35+
*/
36+
public static set strings(value: any) {
37+
this._strings = value;
38+
this._stringsEventDispatcher.fire(null);
39+
}
40+
41+
/**
42+
* returns body dir attribute to determine rtl or ltr
43+
*
44+
* @static
45+
* @returns {string} dir
46+
* @memberof LocalizationHelper
47+
*/
48+
public static getDocumentDirection() {
49+
return document.body.getAttribute('dir') || document.documentElement.getAttribute('dir');
50+
}
51+
52+
/**
53+
* Fires event when LocalizationHelper changes state
54+
*
55+
* @static
56+
* @param {EventHandler<ProvidersChangedState>} event
57+
* @memberof LocalizationHelper
58+
*/
59+
public static onStringsUpdated(event: EventHandler<any>) {
60+
this._stringsEventDispatcher.add(event);
61+
}
62+
63+
public static removeOnStringsUpdated(event: EventHandler<any>) {
64+
this._stringsEventDispatcher.remove(event);
65+
}
66+
67+
public static onDirectionUpdated(event: EventHandler<any>) {
68+
this._directionEventDispatcher.add(event);
69+
this.initDirection();
70+
}
71+
72+
public static removeOnDirectionUpdated(event: EventHandler<any>) {
73+
this._directionEventDispatcher.remove(event);
74+
}
75+
76+
private static _isDirectionInit = false;
77+
78+
/**
79+
* Checks for direction setup and adds mutationObserver
80+
*
81+
* @private
82+
* @static
83+
* @returns
84+
* @memberof LocalizationHelper
85+
*/
86+
private static initDirection() {
87+
if (this._isDirectionInit) {
88+
return;
89+
}
90+
this._isDirectionInit = true;
91+
this.mutationObserver = new MutationObserver(mutations => {
92+
mutations.forEach(mutation => {
93+
if (mutation.attributeName == 'dir') {
94+
this._directionEventDispatcher.fire(null);
95+
}
96+
});
97+
});
98+
const options = { attributes: true, attributeFilter: ['dir'] };
99+
this.mutationObserver.observe(document.body, options);
100+
this.mutationObserver.observe(document.documentElement, options);
101+
}
102+
103+
/**
104+
* Provided helper method to determine localized or defaultString for specific string is returned
105+
*
106+
* @static updateStringsForTag
107+
* @param {string} tagName
108+
* @param stringsObj
109+
* @returns
110+
* @memberof LocalizationHelper
111+
*/
112+
public static updateStringsForTag(tagName: string, stringObj) {
113+
tagName = tagName.toLowerCase();
114+
115+
if (tagName.startsWith('mgt-')) {
116+
tagName = tagName.substring(4);
117+
}
118+
119+
if (this._strings && stringObj) {
120+
//check for top level strings, applied per component, overridden by specific component def
121+
for (let prop of Object.entries(stringObj)) {
122+
if (this._strings[prop[0]]) {
123+
stringObj[prop[0]] = this._strings[prop[0]];
124+
}
125+
}
126+
//strings defined component specific
127+
if (this._strings['_components'] && this._strings['_components'][tagName]) {
128+
let strings: any = this._strings['_components'][tagName];
129+
for (let key of Object.keys(strings)) {
130+
if (stringObj[key]) {
131+
stringObj[key] = strings[key];
132+
}
133+
}
134+
}
135+
}
136+
137+
return stringObj;
138+
}
139+
}

packages/mgt-react/src/generated/react.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type AgendaProps = {
1414
groupByDay?: boolean;
1515
templateConverters?: MgtElement.TemplateContext;
1616
templateContext?: MgtElement.TemplateContext;
17+
direction?: string;
1718
useShadowRoot?: boolean;
1819
mediaQuery?: MgtElement.ComponentMediaQuery;
1920
eventClick?: (e: Event) => void;
@@ -28,6 +29,7 @@ export type GetProps = {
2829
pollingRate?: number;
2930
templateConverters?: MgtElement.TemplateContext;
3031
templateContext?: MgtElement.TemplateContext;
32+
direction?: string;
3133
useShadowRoot?: boolean;
3234
mediaQuery?: MgtElement.ComponentMediaQuery;
3335
dataChange?: (e: Event) => void;
@@ -37,6 +39,7 @@ export type LoginProps = {
3739
userDetails?: IDynamicPerson;
3840
templateConverters?: MgtElement.TemplateContext;
3941
templateContext?: MgtElement.TemplateContext;
42+
direction?: string;
4043
useShadowRoot?: boolean;
4144
mediaQuery?: MgtElement.ComponentMediaQuery;
4245
loginInitiated?: (e: Event) => void;
@@ -58,6 +61,7 @@ export type PeoplePickerProps = {
5861
selectedPeople?: IDynamicPerson[];
5962
templateConverters?: MgtElement.TemplateContext;
6063
templateContext?: MgtElement.TemplateContext;
64+
direction?: string;
6165
useShadowRoot?: boolean;
6266
mediaQuery?: MgtElement.ComponentMediaQuery;
6367
selectionChanged?: (e: Event) => void;
@@ -73,6 +77,7 @@ export type PeopleProps = {
7377
showMax?: number;
7478
templateConverters?: MgtElement.TemplateContext;
7579
templateContext?: MgtElement.TemplateContext;
80+
direction?: string;
7681
useShadowRoot?: boolean;
7782
mediaQuery?: MgtElement.ComponentMediaQuery;
7883
}
@@ -89,6 +94,7 @@ export type PersonCardProps = {
8994
personPresence?: MicrosoftGraphBeta.Presence;
9095
templateConverters?: MgtElement.TemplateContext;
9196
templateContext?: MgtElement.TemplateContext;
97+
direction?: string;
9298
useShadowRoot?: boolean;
9399
mediaQuery?: MgtElement.ComponentMediaQuery;
94100
}
@@ -109,6 +115,7 @@ export type PersonProps = {
109115
avatarSize?: AvatarSize;
110116
templateConverters?: MgtElement.TemplateContext;
111117
templateContext?: MgtElement.TemplateContext;
118+
direction?: string;
112119
useShadowRoot?: boolean;
113120
mediaQuery?: MgtElement.ComponentMediaQuery;
114121
}
@@ -128,6 +135,7 @@ export type TasksProps = {
128135
taskFilter?: TaskFilter;
129136
templateConverters?: MgtElement.TemplateContext;
130137
templateContext?: MgtElement.TemplateContext;
138+
direction?: string;
131139
useShadowRoot?: boolean;
132140
mediaQuery?: MgtElement.ComponentMediaQuery;
133141
taskAdded?: (e: Event) => void;
@@ -140,6 +148,7 @@ export type TeamsChannelPickerProps = {
140148
selectedItem?: SelectedChannel;
141149
templateConverters?: MgtElement.TemplateContext;
142150
templateContext?: MgtElement.TemplateContext;
151+
direction?: string;
143152
useShadowRoot?: boolean;
144153
mediaQuery?: MgtElement.ComponentMediaQuery;
145154
selectionChanged?: (e: Event) => void;

0 commit comments

Comments
 (0)