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(checkbox, radio, toggle, range): stacked labels for form controls #28075

Merged
merged 14 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,13 @@ ion-card-title,prop,mode,"ios" | "md",undefined,false,false
ion-card-title,css-prop,--color

ion-checkbox,shadow
ion-checkbox,prop,alignment,"center" | "start",'center',false,false
ion-checkbox,prop,checked,boolean,false,false,false
ion-checkbox,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-checkbox,prop,disabled,boolean,false,false,false
ion-checkbox,prop,indeterminate,boolean,false,false,false
ion-checkbox,prop,justify,"end" | "space-between" | "start",'space-between',false,false
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-checkbox,prop,legacy,boolean | undefined,undefined,false,false
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
ion-checkbox,prop,name,string,this.inputId,false,false
Expand Down Expand Up @@ -1008,10 +1009,11 @@ ion-progress-bar,part,stream
ion-progress-bar,part,track

ion-radio,shadow
ion-radio,prop,alignment,"center" | "start",'center',false,false
ion-radio,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-radio,prop,disabled,boolean,false,false,false
ion-radio,prop,justify,"end" | "space-between" | "start",'space-between',false,false
ion-radio,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-radio,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-radio,prop,legacy,boolean | undefined,undefined,false,false
ion-radio,prop,mode,"ios" | "md",undefined,false,false
ion-radio,prop,name,string,this.inputId,false,false
Expand All @@ -1038,7 +1040,7 @@ ion-range,prop,debounce,number | undefined,undefined,false,false
ion-range,prop,disabled,boolean,false,false,false
ion-range,prop,dualKnobs,boolean,false,false,false
ion-range,prop,label,string | undefined,undefined,false,false
ion-range,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-range,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-range,prop,legacy,boolean | undefined,undefined,false,false
ion-range,prop,max,number,100,false,false
ion-range,prop,min,number,0,false,false
Expand Down Expand Up @@ -1478,12 +1480,13 @@ ion-toast,part,icon
ion-toast,part,message

ion-toggle,shadow
ion-toggle,prop,alignment,"center" | "start",'center',false,false
ion-toggle,prop,checked,boolean,false,false,false
ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-toggle,prop,disabled,boolean,false,false,false
ion-toggle,prop,enableOnOffLabels,boolean | undefined,config.get('toggleOnOffLabels'),false,false
ion-toggle,prop,justify,"end" | "space-between" | "start",'space-between',false,false
ion-toggle,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-toggle,prop,legacy,boolean | undefined,undefined,false,false
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
ion-toggle,prop,name,string,this.inputId,false,false
Expand Down
56 changes: 40 additions & 16 deletions core/src/components.d.ts

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions core/src/components/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@
@include margin($checkbox-item-label-margin-top, null, $checkbox-item-label-margin-bottom, null);
}

:host(.in-item.checkbox-label-placement-stacked) .label-text-wrapper {
@include margin($checkbox-item-label-margin-top, null, $form-control-label-margin, null);
}

:host(.in-item.checkbox-label-placement-stacked) .native-wrapper {
@include margin(null, null, $checkbox-item-label-margin-bottom, null);
}

/**
* If no label text is placed into the slot
* then the element should be hidden otherwise
Expand Down Expand Up @@ -181,6 +189,17 @@ input {
justify-content: end;
}

// Align Items
// ---------------------------------------------

:host(.checkbox-alignment-start) .checkbox-wrapper {
align-items: start;
}

:host(.checkbox-alignment-center) .checkbox-wrapper {
align-items: center;
}


// Label Placement - Start
// ----------------------------------------------------------------
Expand Down Expand Up @@ -248,6 +267,24 @@ input {
max-width: 200px;
}

// Label Placement - Stacked
// ----------------------------------------------------------------

/**
* Label is on top of the checkbox.
*/
:host(.checkbox-label-placement-stacked) .checkbox-wrapper {
flex-direction: column;
}

:host(.checkbox-label-placement-stacked) .label-text-wrapper {
/**
* The margin between the label and
* the checkbox should be on the bottom
* when the label sits at the top.
*/
@include margin(null, 0, $form-control-label-margin, 0);
}

// Checked / Indeterminate Checkbox
// ---------------------------------------------
Expand Down
12 changes: 11 additions & 1 deletion core/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ export class Checkbox implements ComponentInterface {
* `"start"`: The label will appear to the left of the checkbox in LTR and to the right in RTL.
* `"end"`: The label will appear to the right of the checkbox in LTR and to the left in RTL.
* `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
* `"stacked"`: The label will appear above the checkbox regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' = 'start';
@Prop() labelPlacement: 'start' | 'end' | 'fixed' | 'stacked' = 'start';

/**
* How to pack the label and checkbox within a line.
Expand All @@ -95,6 +96,13 @@ export class Checkbox implements ComponentInterface {
*/
@Prop() justify: 'start' | 'end' | 'space-between' = 'space-between';

/**
* How to control the alignment of the checkbox and label on the cross axis.
* `"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.
* `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
*/
@Prop() alignment: 'start' | 'center' = 'center';

// TODO(FW-3100): remove this
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup.
Expand Down Expand Up @@ -224,6 +232,7 @@ export class Checkbox implements ComponentInterface {
labelPlacement,
name,
value,
alignment,
} = this;
const mode = getIonMode(this);
const path = getSVGPath(mode, indeterminate);
Expand All @@ -240,6 +249,7 @@ export class Checkbox implements ComponentInterface {
'checkbox-indeterminate': indeterminate,
interactive: true,
[`checkbox-justify-${justify}`]: true,
[`checkbox-alignment-${alignment}`]: true,
[`checkbox-label-placement-${labelPlacement}`]: true,
})}
>
Expand Down
17 changes: 17 additions & 0 deletions core/src/components/checkbox/test/item/checkbox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,21 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
await expect(list).toHaveScreenshot(screenshot(`checkbox-long-label-in-item`));
});
});

test.describe(title('checkbox: stacked label in item'), () => {
test('should render margins correctly when using stacked label in item', async ({ page }) => {
await page.setContent(
`
<ion-list>
<ion-item>
<ion-checkbox label-placement="stacked">Enable Notifications</ion-checkbox>
</ion-item>
</ion-list>
`,
config
);
const list = page.locator('ion-list');
await expect(list).toHaveScreenshot(screenshot(`checkbox-stacked-label-in-item`));
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions core/src/components/checkbox/test/item/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ <h2>Justify Space Between</h2>
</div>
</div>

<h1>Placement Stacked</h1>
<div class="grid">
<div class="grid-item">
<h2>Align Start</h2>
<ion-list>
<ion-item>
<ion-checkbox label-placement="stacked" alignment="start">Enable Notifications</ion-checkbox>
</ion-item>
</ion-list>
</div>

<div class="grid-item">
<h2>Align Center</h2>
<ion-list>
<ion-item>
<ion-checkbox label-placement="stacked" alignment="center">Enable Notifications</ion-checkbox>
</ion-item>
</ion-list>
</div>
</div>

<h1>States</h1>
<div class="grid">
<div class="grid-item">
Expand Down
26 changes: 26 additions & 0 deletions core/src/components/checkbox/test/label/checkbox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,31 @@ configs().forEach(({ title, screenshot, config }) => {
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-label-fixed-justify-space-between`));
});
});

test.describe('checkbox: stacked placement', () => {
test('should align the label to the start of the container in the stacked position', async ({ page }) => {
await page.setContent(
`
<ion-checkbox label-placement="stacked" alignment="start" style="width: 200px">This is a long label</ion-checkbox>
`,
config
);

const checkbox = page.locator('ion-checkbox');
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-label-stacked-align-start`));
});

test('should align the label to the center of the container in the stacked position', async ({ page }) => {
await page.setContent(
`
<ion-checkbox label-placement="stacked" alignment="center" style="width: 200px">This is a long label</ion-checkbox>
`,
config
);

const checkbox = page.locator('ion-checkbox');
await expect(checkbox).toHaveScreenshot(screenshot(`checkbox-label-stacked-align-center`));
});
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions core/src/components/checkbox/test/label/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ <h2>Justify Space Between</h2>
<ion-checkbox label-placement="fixed" justify="space-between">Enable Notifications</ion-checkbox>
</div>
</div>

<h1>Placement Stacked</h1>
<div class="grid">
<div class="grid-item">
<h2>Align Start</h2>
<ion-checkbox label-placement="stacked" alignment="start">Enable Notifications</ion-checkbox>
</div>

<div class="grid-item">
<h2>Align Center</h2>
<ion-checkbox label-placement="stacked" alignment="center">Enable Notifications</ion-checkbox>
</div>
</div>
</ion-content>
</ion-app>
</body>
Expand Down
38 changes: 38 additions & 0 deletions core/src/components/radio/radio.scss
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ input {
@include margin($radio-item-label-margin-top, null, $radio-item-label-margin-bottom, null);
}

:host(.in-item.radio-label-placement-stacked) .label-text-wrapper {
@include margin($radio-item-label-margin-top, null, $form-control-label-margin, null);
}

:host(.in-item.radio-label-placement-stacked) .native-wrapper {
@include margin(null, null, $radio-item-label-margin-bottom, null);
}

/**
* If no label text is placed into the slot
* then the element should be hidden otherwise
Expand Down Expand Up @@ -160,6 +168,17 @@ input {
justify-content: end;
}

// Radio Align
// --------------------------------------------------

:host(.radio-alignment-start) .radio-wrapper {
align-items: start;
}

:host(.radio-alignment-center) .radio-wrapper {
align-items: center;
}

// Radio Label Placement - Start
// ----------------------------------------------------------------

Expand Down Expand Up @@ -222,3 +241,22 @@ input {
width: 100px;
min-width: 100px;
}

// Radio Label Placement - Stacked
// ----------------------------------------------------------------

/**
* Label is on top of the radio.
*/
:host(.radio-label-placement-stacked) .radio-wrapper {
flex-direction: column;
}

:host(.radio-label-placement-stacked) .label-text-wrapper {
/**
* The margin between the label and
* the radio should be on the bottom
* when the label sits on top.
*/
@include margin(null, 0, $form-control-label-margin, 0);
}
13 changes: 11 additions & 2 deletions core/src/components/radio/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ export class Radio implements ComponentInterface {
* `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL.
* `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL.
* `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
* `"stacked"`: The label will appear above the radio regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
*/
@Prop() labelPlacement: 'start' | 'end' | 'fixed' = 'start';
@Prop() labelPlacement: 'start' | 'end' | 'fixed' | 'stacked' = 'start';

// TODO FW-3125: Remove the legacy property and implementation
/**
Expand All @@ -110,6 +111,13 @@ export class Radio implements ComponentInterface {
*/
@Prop() justify: 'start' | 'end' | 'space-between' = 'space-between';

/**
* How to control the alignment of the radio and label on the cross axis.
* `"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.
* `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
*/
@Prop() alignment: 'start' | 'center' = 'center';

/**
* Emitted when the styles change.
* @internal
Expand Down Expand Up @@ -250,7 +258,7 @@ export class Radio implements ComponentInterface {
}

private renderRadio() {
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex } = this;
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex, alignment } = this;
const mode = getIonMode(this);
const inItem = hostContext('ion-item', el);

Expand All @@ -265,6 +273,7 @@ export class Radio implements ComponentInterface {
'radio-checked': checked,
'radio-disabled': disabled,
[`radio-justify-${justify}`]: true,
[`radio-alignment-${alignment}`]: true,
[`radio-label-placement-${labelPlacement}`]: true,
// Focus and active styling should not apply when the radio is in an item
'ion-activatable': !inItem,
Expand Down
25 changes: 25 additions & 0 deletions core/src/components/radio/test/item/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,31 @@ <h2>Justify Space Between</h2>
</div>
</div>

<h1>Placement Stacked</h1>
<div class="grid">
<div class="grid-item">
<h2>Align Start</h2>
<ion-list>
<ion-radio-group>
<ion-item>
<ion-radio label-placement="stacked" alignment="start">Enable Notifications</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</div>

<div class="grid-item">
<h2>Align Center</h2>
<ion-list>
<ion-radio-group>
<ion-item>
<ion-radio label-placement="stacked" alignment="center">Enable Notifications</ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
</div>
</div>

<h1>States</h1>
<div class="grid">
<ion-list>
Expand Down
Loading
Loading