feat(a11y): add dynamic font scaling (#28314)
Issue number: resolves #24638, resolves #18592 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Developers have requested that Ionic Framework support the dynamic type feature on iOS for accessibility purposes. Ionic applications do not respond to font scaling on iOS which can create inaccessible applications particularly for users with low vision. Ionic apps on Android devices currently support the Android equivalent due to functionality in the Chromium webview. Developers have also requested a way of adjusting the fonts in their Ionic UI components consistently. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Ionic components now use `rem` instead of `px` where appropriate. This means devs can change the font size on `html` and the text in supported Ionic components will scale up/down appropriately - Add support for Dynamic Type on iOS (the iOS version of Dynamic Font Scaling) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com> Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com> Co-authored-by: Shawn Taylor <shawn@ionic.io> Co-authored-by: ionitron <hi@ionicframework.com> Co-authored-by: Sean Perkins <sean@ionic.io> Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com> Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
@ -255,13 +255,19 @@
|
||||
}
|
||||
|
||||
.alert-button {
|
||||
/**
|
||||
* This padding ensures that alerts
|
||||
* with multiple buttons render on separate lines
|
||||
* so the text does not run up against the edge of the button.
|
||||
*/
|
||||
@include padding-horizontal($alert-ios-button-padding);
|
||||
@include margin($alert-ios-button-margin);
|
||||
@include border-radius($alert-ios-button-border-radius);
|
||||
|
||||
flex: $alert-ios-button-flex;
|
||||
|
||||
min-width: $alert-ios-button-min-width;
|
||||
height: $alert-ios-button-min-height;
|
||||
height: $alert-ios-button-height;
|
||||
|
||||
border-top: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color;
|
||||
border-right: $alert-ios-button-border-width $alert-ios-button-border-style $alert-ios-button-border-color;
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - Font size of the alert
|
||||
$alert-ios-font-size: 14px !default;
|
||||
$alert-ios-font-size: dynamic-font-min(1, 14px) !default;
|
||||
|
||||
/// @prop - Max width of the alert
|
||||
$alert-ios-max-width: 270px !default;
|
||||
$alert-ios-max-width: dynamic-font-clamp(1, 270px, 1.2) !default;
|
||||
|
||||
/// @prop - Border radius of the alert
|
||||
$alert-ios-border-radius: 13px !default;
|
||||
@ -48,13 +48,13 @@ $alert-ios-title-color: $text-color !default;
|
||||
$alert-ios-title-margin-top: 8px !default;
|
||||
|
||||
/// @prop - Font size of the alert title
|
||||
$alert-ios-title-font-size: 17px !default;
|
||||
$alert-ios-title-font-size: dynamic-font-min(1, 17px) !default;
|
||||
|
||||
/// @prop - Font weight of the alert title
|
||||
$alert-ios-title-font-weight: 600 !default;
|
||||
|
||||
/// @prop - Font size of the alert sub title
|
||||
$alert-ios-sub-title-font-size: 14px !default;
|
||||
$alert-ios-sub-title-font-size: dynamic-font-min(1, 14px) !default;
|
||||
|
||||
/// @prop - Text color of the alert sub title
|
||||
$alert-ios-sub-title-text-color: $text-color-step-400 !default;
|
||||
@ -72,7 +72,7 @@ $alert-ios-message-padding-bottom: 21px !default;
|
||||
$alert-ios-message-padding-start: $alert-ios-message-padding-end !default;
|
||||
|
||||
/// @prop - Font size of the alert message
|
||||
$alert-ios-message-font-size: 13px !default;
|
||||
$alert-ios-message-font-size: dynamic-font-min(1, 13px) !default;
|
||||
|
||||
/// @prop - Text align of the alert message
|
||||
$alert-ios-message-text-align: center !default;
|
||||
@ -137,11 +137,23 @@ $alert-ios-button-margin: 0 !default;
|
||||
/// @prop - Min width of the alert button
|
||||
$alert-ios-button-min-width: 50% !default;
|
||||
|
||||
/// @prop - Minimum height of the alert button
|
||||
$alert-ios-button-min-height: 44px !default;
|
||||
/// @prop - Height of the alert button
|
||||
/**
|
||||
* We want the height of the button to
|
||||
* scale with the text so the next never runs
|
||||
* into the edge of the button. We change the height
|
||||
* instead of adding padding because we would need to offset
|
||||
* the height the padding and the border. Since the border uses
|
||||
* a hairline (<1px) width, this will cause subpixel rendering
|
||||
* differences across browsers.
|
||||
*/
|
||||
$alert-ios-button-height: dynamic-font-min(1, 44px) !default;
|
||||
|
||||
/// @prop - Padding of the alert button
|
||||
$alert-ios-button-padding: 8px !default;
|
||||
|
||||
/// @prop - Font size of the alert button
|
||||
$alert-ios-button-font-size: 17px !default;
|
||||
$alert-ios-button-font-size: dynamic-font-min(1, 17px) !default;
|
||||
|
||||
/// @prop - Color of the text in the alert button
|
||||
$alert-ios-button-text-color: ion-color(primary, base) !default;
|
||||
@ -252,10 +264,10 @@ $alert-ios-checkbox-margin-bottom: 10px !default;
|
||||
$alert-ios-checkbox-margin-start: 16px !default;
|
||||
|
||||
/// @prop - Size of the checkbox in the alert
|
||||
$alert-ios-checkbox-size: 24px !default;
|
||||
$alert-ios-checkbox-size: dynamic-font-max(24px, 2.75) !default;
|
||||
|
||||
/// @prop - Border width of the checkbox in the alert
|
||||
$alert-ios-checkbox-border-width: 1px !default;
|
||||
$alert-ios-checkbox-border-width: dynamic-font(1px) !default;
|
||||
|
||||
/// @prop - Border style of the checkbox in the alert
|
||||
$alert-ios-checkbox-border-style: solid !default;
|
||||
@ -276,16 +288,16 @@ $alert-ios-checkbox-background-color-off: $item-ios-background !de
|
||||
$alert-ios-checkbox-background-color-on: ion-color(primary, base) !default;
|
||||
|
||||
/// @prop - Top of the icon in the checkbox alert
|
||||
$alert-ios-checkbox-icon-top: math.div($alert-ios-checkbox-size, 6) !default;
|
||||
$alert-ios-checkbox-icon-top: calc($alert-ios-checkbox-size / 6) !default;
|
||||
|
||||
/// @prop - Start of the icon in the checkbox alert
|
||||
$alert-ios-checkbox-icon-start: math.div($alert-ios-checkbox-size, 3) + 1px !default;
|
||||
$alert-ios-checkbox-icon-start: calc($alert-ios-checkbox-size / 3 + 1px) !default;
|
||||
|
||||
/// @prop - Width of the icon in the checkbox alert
|
||||
$alert-ios-checkbox-icon-width: math.div($alert-ios-checkbox-size, 6) + 1px !default;
|
||||
$alert-ios-checkbox-icon-width: calc($alert-ios-checkbox-size / 6 + 1px) !default;
|
||||
|
||||
/// @prop - Height of the icon in the checkbox alert
|
||||
$alert-ios-checkbox-icon-height: $alert-ios-checkbox-size * 0.5 !default;
|
||||
$alert-ios-checkbox-icon-height: calc($alert-ios-checkbox-size * 0.5) !default;
|
||||
|
||||
/// @prop - Border width of the icon in the checkbox alert
|
||||
$alert-ios-checkbox-icon-border-width: $alert-ios-checkbox-border-width !default;
|
||||
|
||||
@ -192,6 +192,10 @@
|
||||
|
||||
flex: 1;
|
||||
|
||||
// Required for the checkbox icon to stay on the screen without
|
||||
// being squished when the font size scales up.
|
||||
width: calc(100% - $alert-md-checkbox-label-padding-start);
|
||||
|
||||
color: $alert-md-checkbox-label-text-color;
|
||||
|
||||
font-size: $alert-md-checkbox-label-font-size;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - Font size of the alert
|
||||
$alert-md-font-size: 14px !default;
|
||||
$alert-md-font-size: dynamic-font(14px) !default;
|
||||
|
||||
/// @prop - Max width of the alert
|
||||
$alert-md-max-width: 280px !default;
|
||||
@ -38,13 +38,13 @@ $alert-md-head-text-align: start !default;
|
||||
$alert-md-title-color: $text-color !default;
|
||||
|
||||
/// @prop - Font size of the alert title
|
||||
$alert-md-title-font-size: 20px !default;
|
||||
$alert-md-title-font-size: dynamic-font(20px) !default;
|
||||
|
||||
/// @prop - Font weight of the alert title
|
||||
$alert-md-title-font-weight: 500 !default;
|
||||
|
||||
/// @prop - Font size of the alert sub title
|
||||
$alert-md-sub-title-font-size: 16px !default;
|
||||
$alert-md-sub-title-font-size: dynamic-font(16px) !default;
|
||||
|
||||
/// @prop - Text color of the alert sub title
|
||||
$alert-md-sub-title-text-color: $text-color !default;
|
||||
@ -62,7 +62,7 @@ $alert-md-message-padding-bottom: $alert-md-message-padding-top !def
|
||||
$alert-md-message-padding-start: $alert-md-message-padding-end !default;
|
||||
|
||||
/// @prop - Font size of the alert message
|
||||
$alert-md-message-font-size: 16px !default;
|
||||
$alert-md-message-font-size: dynamic-font(16px) !default;
|
||||
|
||||
/// @prop - Text color of the alert message
|
||||
$alert-md-message-text-color: $text-color-step-450 !default;
|
||||
@ -239,7 +239,7 @@ $alert-md-radio-label-padding-bottom: $alert-md-radio-label-padding-top
|
||||
$alert-md-radio-label-padding-start: $alert-md-radio-label-padding-end + 26px !default;
|
||||
|
||||
/// @prop - Font size of the label for the radio alert
|
||||
$alert-md-radio-label-font-size: 16px !default;
|
||||
$alert-md-radio-label-font-size: dynamic-font(16px) !default;
|
||||
|
||||
/// @prop - Text color of the label for the radio alert
|
||||
$alert-md-radio-label-text-color: $text-color-step-150 !default;
|
||||
@ -314,7 +314,7 @@ $alert-md-checkbox-label-padding-start: $alert-md-checkbox-label-padding-e
|
||||
$alert-md-checkbox-label-text-color: $text-color-step-150 !default;
|
||||
|
||||
/// @prop - Font size of the label for the checkbox in the alert
|
||||
$alert-md-checkbox-label-font-size: 16px !default;
|
||||
$alert-md-checkbox-label-font-size: dynamic-font(16px) !default;
|
||||
|
||||
/// @prop - Height of the tappable inputs in the checkbox alert
|
||||
$alert-md-tappable-height: $item-md-min-height !default;
|
||||
|
||||
@ -10,10 +10,10 @@ $alert-min-width: 250px !default;
|
||||
$alert-max-height: 90% !default;
|
||||
|
||||
/// @prop - Line height of the alert button
|
||||
$alert-button-line-height: 20px !default;
|
||||
$alert-button-line-height: dynamic-font(20px) !default;
|
||||
|
||||
/// @prop - Font size of the alert button
|
||||
$alert-button-font-size: 14px !default;
|
||||
$alert-button-font-size: dynamic-font(14px) !default;
|
||||
|
||||
/// @prop - Minimum height of a textarea in the alert
|
||||
$alert-input-min-height: 37px !default;
|
||||
|
||||
@ -95,3 +95,139 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('alert: font scaling'), () => {
|
||||
test('should scale text on larger font sizes', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
html {
|
||||
font-size: 36px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<ion-alert header="Header" sub-header="Sub Header" message="Message"></ion-alert>
|
||||
|
||||
<script>
|
||||
const alert = document.querySelector('ion-alert');
|
||||
alert.buttons = ['Ok', 'Cancel'];
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await alert.evaluate((el: HTMLIonAlertElement) => el.present());
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`alert-scale`));
|
||||
});
|
||||
test('should scale text on larger font sizes with checkboxes', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
html {
|
||||
font-size: 36px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<ion-alert header="Header" sub-header="Sub Header" message="Message"></ion-alert>
|
||||
|
||||
<script>
|
||||
const alert = document.querySelector('ion-alert');
|
||||
alert.inputs = [
|
||||
{ type: 'checkbox', value: 'a', label: 'Checkbox A', checked: true },
|
||||
{ type: 'checkbox', value: 'b', label: 'Checkbox B' },
|
||||
{ type: 'checkbox', value: 'c', label: 'Checkbox C' },
|
||||
{ type: 'checkbox', value: 'd', label: 'Checkbox D' },
|
||||
];
|
||||
alert.buttons = ['Ok', 'Cancel'];
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await alert.evaluate((el: HTMLIonAlertElement) => el.present());
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`alert-checkbox-scale`));
|
||||
});
|
||||
test('should scale text on larger font sizes with radios', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
html {
|
||||
font-size: 36px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<ion-alert header="Header" sub-header="Sub Header" message="Message"></ion-alert>
|
||||
|
||||
<script>
|
||||
const alert = document.querySelector('ion-alert');
|
||||
alert.inputs = [
|
||||
{ type: 'radio', value: 'a', label: 'Radio A', checked: true },
|
||||
{ type: 'radio', value: 'b', label: 'Radio B' },
|
||||
{ type: 'radio', value: 'c', label: 'Radio C' },
|
||||
{ type: 'radio', value: 'd', label: 'Radio D' },
|
||||
];
|
||||
alert.buttons = ['Ok', 'Cancel'];
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await alert.evaluate((el: HTMLIonAlertElement) => el.present());
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`alert-radio-scale`));
|
||||
});
|
||||
test('should scale text on larger font sizes with text fields', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
html {
|
||||
font-size: 36px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<ion-alert header="Header" sub-header="Sub Header" message="Message"></ion-alert>
|
||||
|
||||
<script>
|
||||
const alert = document.querySelector('ion-alert');
|
||||
alert.inputs = [
|
||||
{ type: 'text', value: 'My Input', label: 'Input' },
|
||||
{ type: 'textarea', value: 'My Textarea', label: 'Textarea' },
|
||||
];
|
||||
alert.buttons = ['Ok', 'Cancel'];
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await alert.evaluate((el: HTMLIonAlertElement) => el.present());
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`alert-text-fields-scale`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 24 KiB |