Compare commits

...

13 Commits

Author SHA1 Message Date
Liam DeBeasi
bc64f72109 chore: sync with main
chore: sync with main
2023-09-11 10:04:03 -04:00
Liam DeBeasi
474308618d Merge remote-tracking branch 'origin/main' into sync-feat-74 2023-09-11 09:23:16 -04:00
hoi4
19f3bb23fd feat: export TransitionOptions interface and getIonPageElement (#28140)
Issue number: resolves #28137

---------

Changes according to [this
comment](https://github.com/ionic-team/ionic-framework/issues/28137#issuecomment-1710283096)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

@liamdebeasi Sorry for replacing the previous PR. I only copied the main
branch to my fork so I couldn't rebase properly. I am unfortunately not
extremely familiar with Github.

---------

Co-authored-by: Philipp Heuer <philipp@studysmarter.de>
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2023-09-11 08:53:56 -04:00
Liam DeBeasi
cd8d5091a1 feat(datetime): add disabled part (#28134) 2023-09-07 15:57:38 -04:00
Maria Hutt
e0542a7867 fix(menu): remove app dir from safe area padding (#28123)
Issue number: internal

---------

<!-- 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. -->

The `--ion-safe-area-left` and `--ion-safe-area-right` variables in
`ion-menu` are being set as if they use the app's direction. It's been
determined that safe area is not logical and uses the device's
direction. The current implementation is adding padding in the wrong
sides for `ion-toolbar` and `ion-content` within a `ion-menu`.

Additionally, `ion-menu` does not use the entire screen so the safe area
only needs to be applied to the side that is touching the device screen.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Set the `--ion-safe-area-left` and `--ion-safe-area-right` variables
to the correct values based on the device's direction.
- Padding is only added to the side that is not in the safe area.
- `ion-toolbar` is adding `--ion-safe-area-left` and
`--ion-safe-area-right` based on the device's direction.
- `ion-toolbar` can now inherit the correct values from
`--ion-safe-area-left` and `--ion-safe-area-right`.
- `ion-content` can now inherit the correct values from
`--ion-safe-area-left` and `--ion-safe-area-right`.

## 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. -->

Dev build: 7.3.4-dev.11694015543.18bc484f
2023-09-07 18:33:20 +00:00
Brandy Carney
79b005da70 feat(datetime): add parts for calendar day, active, and today (#27641)
Issue number: resolves #25340

---------

- Exposes the following parts for a calendar day: `calendar-day`,
`today`, and `active`
- Combines the `calendar-day-highlight` element with the `calendar-day`
element so developers don't have to know to style two different elements
& we don't have to expose them as separate parts
- Improves height parity of the calendar day across browsers
- Updates the `custom` e2e test to include an example of styling days
using the newly exposed CSS parts
- Adds tests for the focus states of the calendar day
2023-09-06 11:58:41 -04:00
Maria Hutt
e6c7bb60e7 feat(checkbox, radio, toggle, range): stacked labels for form controls (#28075)
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
Co-authored-by: ionitron <hi@ionicframework.com>
2023-09-01 09:30:59 -07:00
Liam DeBeasi
cbafa6b40d chore: sync with main
chore: sync with main
2023-09-01 10:12:12 -04:00
Liam DeBeasi
e6c09291f5 Merge remote-tracking branch 'origin/main' into sync-feature-7.4 2023-09-01 09:51:09 -04:00
Liam DeBeasi
21b0731cf2 chore: sync with main
chore: sync with main
2023-08-29 09:23:58 -05:00
Liam DeBeasi
b655067867 chore: sync with main 2023-08-29 09:58:37 -04:00
Amanda Johnston
32244fbdd1 fix(datetime): scroll to newly selected date when value changes (#27806)
Issue number: Resolves #26391

---------

<!-- 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. -->

When updating the `value` programmatically on an `ion-datetime` after it
has already been created:
- With grid style: The selected date visually updates, but the calendar
does not scroll to the newly selected month.
- With wheel style: The selected date does not visually update, i.e. the
wheels do not move to show the newly selected date.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Grid style datetimes now scroll to the selected date using the same
animation as when clicking the next/prev month buttons.
- This animation mirrors the behavior in both MUI and native iOS. See
the [design
doc](https://github.com/ionic-team/ionic-framework-design-documents/blob/main/projects/ionic-framework/components/datetime/0003-datetime-async-value.md)
for more information and screen recordings.
- The animation will not occur if the month/year did not change, or when
the datetime is hidden.
- Wheel style datetimes now visually update to the selected date. No
animation occurs, also mirroring native.
- The `parseDate` util has also had its type signatures updated to
account for returning `undefined` when the date string is improperly
formatted. This was missed when the util was refactored to support
multiple date selection.

## 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. -->

- Docs PR: https://github.com/ionic-team/ionic-docs/pull/3053
- While this can technically be considered a bug fix, we are merging it
into a feature branch for safety; it's a fairly significant change to
how datetime behaves, and may interfere with custom logic when updating
a datetime's value async.
- Jumping to the newly selected value is handled by replacing everything
[here](https://github.com/ionic-team/ionic-framework/pull/27806/files#diff-4a407530c60e3cf72bcc11acdd21c4803a94bf47ea81b99e757db1c93d2735b8L364-L407)
with `processValue()`. This covers both wheel and grid datetimes.
- `activePartsClone` as a whole was also removed. It was added in
https://github.com/ionic-team/ionic-framework/pull/24244 to enable
changing `activeParts` without triggering a rerender (and thus jumping
to the new value) but since we now want to do that jump, the clone is no
longer needed.
- The animation code might be tricky to follow, so I recorded going
through it:
https://github.com/ionic-team/ionic-framework/assets/90629384/1afa5762-f493-441a-b662-f0429f2d86a7
2023-08-23 13:49:19 -05:00
Liam DeBeasi
ae9f1ab43e refactor(toast): deprecate cssClass on ToastButton (#27959) 2023-08-23 09:21:20 -04:00
340 changed files with 1292 additions and 304 deletions

View File

@@ -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
@@ -427,6 +428,10 @@ ion-datetime,css-prop,--background-rgb
ion-datetime,css-prop,--title-color
ion-datetime,css-prop,--wheel-fade-background-rgb
ion-datetime,css-prop,--wheel-highlight-background
ion-datetime,part,calendar-day
ion-datetime,part,calendar-day active
ion-datetime,part,calendar-day disabled
ion-datetime,part,calendar-day today
ion-datetime,part,month-year-button
ion-datetime,part,time-button
ion-datetime,part,time-button active
@@ -1008,10 +1013,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
@@ -1038,7 +1044,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
@@ -1478,12 +1484,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

View File

@@ -602,6 +602,10 @@ export namespace Components {
"mode"?: "ios" | "md";
}
interface IonCheckbox {
/**
* 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.
*/
"alignment": 'start' | 'center';
/**
* If `true`, the checkbox is selected.
*/
@@ -623,9 +627,9 @@ export namespace Components {
*/
"justify": 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the checkbox. `"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 ("...").
* Where to place the label relative to the checkbox. `"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.
*/
"labelPlacement": 'start' | 'end' | 'fixed';
"labelPlacement": 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt checkboxes in to the modern form markup when they are using either the `aria-label` attribute or have text in the default slot. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -2216,6 +2220,10 @@ export namespace Components {
"value": number;
}
interface IonRadio {
/**
* 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.
*/
"alignment": 'start' | 'center';
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
@@ -2229,9 +2237,9 @@ export namespace Components {
*/
"justify": 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the radio. `"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 ("...").
* Where to place the label relative to the radio. `"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.
*/
"labelPlacement": 'start' | 'end' | 'fixed';
"labelPlacement": 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the default slot that contains the label text. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -2291,9 +2299,9 @@ export namespace Components {
*/
"label"?: string;
/**
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range 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 ("...").
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range 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 range regardless of the direction.
*/
"labelPlacement": 'start' | 'end' | 'fixed';
"labelPlacement": 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -3165,6 +3173,10 @@ export namespace Components {
"trigger": string | undefined;
}
interface IonToggle {
/**
* How to control the alignment of the toggle 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.
*/
"alignment": 'start' | 'center';
/**
* If `true`, the toggle is selected.
*/
@@ -3186,9 +3198,9 @@ export namespace Components {
*/
"justify": 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the toggle in LTR and to the right in RTL. `"end"`: The label will appear to the right of the toggle 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 ("...").
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the toggle in LTR and to the right in RTL. `"end"`: The label will appear to the right of the toggle 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 toggle regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
*/
"labelPlacement": 'start' | 'end' | 'fixed';
"labelPlacement": 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the default slot that contains the label text. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -4625,6 +4637,10 @@ declare namespace LocalJSX {
"mode"?: "ios" | "md";
}
interface IonCheckbox {
/**
* 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.
*/
"alignment"?: 'start' | 'center';
/**
* If `true`, the checkbox is selected.
*/
@@ -4646,9 +4662,9 @@ declare namespace LocalJSX {
*/
"justify"?: 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the checkbox. `"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 ("...").
* Where to place the label relative to the checkbox. `"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.
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
"labelPlacement"?: 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt checkboxes in to the modern form markup when they are using either the `aria-label` attribute or have text in the default slot. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -6226,6 +6242,10 @@ declare namespace LocalJSX {
"value"?: number;
}
interface IonRadio {
/**
* 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.
*/
"alignment"?: 'start' | 'center';
/**
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
@@ -6239,9 +6259,9 @@ declare namespace LocalJSX {
*/
"justify"?: 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the radio. `"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 ("...").
* Where to place the label relative to the radio. `"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.
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
"labelPlacement"?: 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the default slot that contains the label text. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -6319,9 +6339,9 @@ declare namespace LocalJSX {
*/
"label"?: string;
/**
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range 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 ("...").
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range 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 range regardless of the direction.
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
"labelPlacement"?: 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/
@@ -7298,6 +7318,10 @@ declare namespace LocalJSX {
"trigger"?: string | undefined;
}
interface IonToggle {
/**
* How to control the alignment of the toggle 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.
*/
"alignment"?: 'start' | 'center';
/**
* If `true`, the toggle is selected.
*/
@@ -7319,9 +7343,9 @@ declare namespace LocalJSX {
*/
"justify"?: 'start' | 'end' | 'space-between';
/**
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the toggle in LTR and to the right in RTL. `"end"`: The label will appear to the right of the toggle 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 ("...").
* Where to place the label relative to the input. `"start"`: The label will appear to the left of the toggle in LTR and to the right in RTL. `"end"`: The label will appear to the right of the toggle 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 toggle regardless of the direction. The alignment of the label can be controlled with the `alignment` property.
*/
"labelPlacement"?: 'start' | 'end' | 'fixed';
"labelPlacement"?: 'start' | 'end' | 'fixed' | 'stacked';
/**
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the default slot that contains the label text. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
*/

View File

@@ -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
@@ -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
// ----------------------------------------------------------------
@@ -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
// ---------------------------------------------

View File

@@ -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.
@@ -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.
@@ -224,6 +232,7 @@ export class Checkbox implements ComponentInterface {
labelPlacement,
name,
value,
alignment,
} = this;
const mode = getIonMode(this);
const path = getSVGPath(mode, indeterminate);
@@ -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,
})}
>

View File

@@ -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`));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -137,6 +137,27 @@
</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">

View File

@@ -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`));
});
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -104,6 +104,19 @@
<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>

View File

@@ -206,6 +206,10 @@ export class DatetimeButton implements ComponentInterface {
*/
const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
if (!parsedDatetimes) {
return;
}
/**
* If developers incorrectly use multiple="true"
* with non "date" datetimes, then just select

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -20,7 +20,7 @@
@include padding($datetime-ios-padding, $datetime-ios-padding, $datetime-ios-padding, $datetime-ios-padding);
border-bottom: $datetime-ios-border-color;
font-size: 14px;
}
@@ -85,27 +85,32 @@
*/
@include padding($datetime-ios-padding * 0.5, $datetime-ios-padding * 0.5, $datetime-ios-padding * 0.5, $datetime-ios-padding * 0.5);
align-items: center;
height: calc(100% - #{$datetime-ios-padding});
}
:host .calendar-day-wrapper {
@include padding(4px);
// This is required so that the calendar day wrapper
// will collapse instead of expanding to fill the button
height: 0;
min-height: 16px;
}
:host .calendar-day {
width: $datetime-ios-day-width;
min-width: $datetime-ios-day-width;
height: $datetime-ios-day-height;
font-size: 20px;
}
.calendar-day:focus .calendar-day-highlight,
.calendar-day.calendar-day-active .calendar-day-highlight {
opacity: 0.2;
}
.calendar-day.calendar-day-active .calendar-day-highlight {
background: current-color(base);
}
// !important is needed here to overwrite custom highlight background, which is inline.
// Does not apply to the active state because highlights aren't applied at all there.
.calendar-day:focus .calendar-day-highlight {
/* stylelint-disable-next-line declaration-no-important */
background: current-color(base) !important;
.calendar-day.calendar-day-active {
background: current-color(base, 0.2);
}
/**
@@ -135,12 +140,6 @@
color: current-color(contrast);
}
.calendar-day.calendar-day-today.calendar-day-active .calendar-day-highlight {
background: current-color(base);
opacity: 1;
}
// Time / Header
// -----------------------------------
:host .datetime-time {

View File

@@ -15,3 +15,9 @@ $datetime-ios-time-width: 68px !default;
/// @prop - Border radius of the time picker
$datetime-ios-time-border-radius: 8px !default;
/// @prop - Width of the calendar day
$datetime-ios-day-width: 40px !default;
/// @prop - Height of the calendar day
$datetime-ios-day-height: $datetime-ios-day-width !default;

View File

@@ -69,27 +69,22 @@
// Individual day button in month
:host .calendar-day {
@include padding(13px, 0, 13px, 0px);
width: $datetime-md-day-width;
min-width: $datetime-md-day-width;
height: $datetime-md-day-height;
font-size: $datetime-md-calendar-item-font-size;
}
.calendar-day:focus .calendar-day-highlight {
background: current-color(base, 0.2);
box-shadow: 0px 0px 0px 4px current-color(base, 0.2);
}
/**
* Day that today but not selected
* should have ion-color for text color.
*/
:host .calendar-day.calendar-day-today {
color: current-color(base);
}
.calendar-day.calendar-day-today .calendar-day-highlight {
border: 1px solid current-color(base);
color: current-color(base);
}
/**
@@ -101,7 +96,7 @@
color: current-color(contrast);
}
.calendar-day.calendar-day-active .calendar-day-highlight {
.calendar-day.calendar-day-active {
border: 1px solid current-color(base);
background: current-color(base);

View File

@@ -15,3 +15,9 @@ $datetime-md-header-padding: 20px !default;
/// @prop - Padding for content
$datetime-md-padding: 16px !default;
/// @prop - Width of the calendar day
$datetime-md-day-width: 42px !default;
/// @prop - Height of the calendar day
$datetime-md-day-height: $datetime-md-day-width !default;

View File

@@ -290,6 +290,10 @@ ion-picker-column-internal {
}
:host .calendar-body .calendar-month {
display: flex;
flex-flow: column;
/**
* Swiping should snap to at
* most one month at a time.
@@ -325,13 +329,31 @@ ion-picker-column-internal {
grid-template-columns: repeat(7, 1fr);
}
:host .calendar-day-wrapper {
display: flex;
align-items: center;
justify-content: center;
// Adding a min width and min height allows
// it to shrink smaller than its content
// which keeps the calendar day highlight
// larger while letting the grid items shrink
min-width: 0;
min-height: 0;
overflow: visible;
}
/**
* Center the day text vertically
* and horizontally within its grid cell.
*/
:host .calendar-day {
@include padding(0px, 0px, 0px, 0px);
@include margin(0px, 0px, 0px, 0px);
@include border-radius(50%);
@include padding(0px);
@include margin(0px);
display: flex;
@@ -362,16 +384,10 @@ ion-picker-column-internal {
opacity: 0.4;
}
.calendar-day-highlight {
@include border-radius(32px, 32px, 32px, 32px);
@include padding(4px, 4px, 4px, 4px);
.calendar-day:focus {
background: current-color(base, 0.2);
position: absolute;
width: 32px;
height: 32px;
z-index: -1;
box-shadow: 0px 0px 0px 4px current-color(base, 0.2);
}
// Time / Header

View File

@@ -85,6 +85,12 @@ import {
*
* @part month-year-button - The button that opens the month/year picker when
* using a grid style layout.
*
* @part calendar-day - The individual buttons that display a day inside of the datetime
* calendar.
* @part calendar-day active - The currently selected calendar day.
* @part calendar-day today - The calendar day that contains the current day.
* @part calendar-day disabled - The calendar day that is disabled.
*/
@Component({
tag: 'ion-datetime',
@@ -117,11 +123,7 @@ export class Datetime implements ComponentInterface {
private prevPresentation: string | null = null;
/**
* Duplicate reference to `activeParts` that does not trigger a re-render of the component.
* Allows caching an instance of the `activeParts` in between render cycles.
*/
private activePartsClone: DatetimeParts | DatetimeParts[] = [];
private resolveForceDateScrolling?: () => void;
@State() showMonthAndYear = false;
@@ -140,6 +142,17 @@ export class Datetime implements ComponentInterface {
@State() isTimePopoverOpen = false;
/**
* When defined, will force the datetime to render the month
* containing the specified date. Currently, this should only
* be used to enable immediately auto-scrolling to the new month,
* and should then be reset to undefined once the transition is
* finished and the forced month is now in view.
*
* Applies to grid-style datetimes only.
*/
@State() forceRenderDate?: DatetimeParts;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -221,6 +234,12 @@ export class Datetime implements ComponentInterface {
*/
@Prop() presentation: DatetimePresentation = 'date-time';
private get isGridStyle() {
const { presentation, preferWheel } = this;
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
return hasDatePresentation && !preferWheel;
}
/**
* The text to display on the picker's cancel button.
*/
@@ -302,11 +321,6 @@ export class Datetime implements ComponentInterface {
this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
}
@Watch('activeParts')
protected activePartsChanged() {
this.activePartsClone = this.activeParts;
}
/**
* The locale to use for `ion-datetime`. This
* impacts month and day name formatting.
@@ -356,54 +370,11 @@ export class Datetime implements ComponentInterface {
* Update the datetime value when the value changes
*/
@Watch('value')
protected valueChanged() {
const { value, minParts, maxParts, workingParts } = this;
protected async valueChanged() {
const { value } = this;
if (this.hasValue()) {
this.warnIfIncorrectValueUsage();
/**
* Clones the value of the `activeParts` to the private clone, to update
* the date display on the current render cycle without causing another render.
*
* This allows us to update the current value's date/time display without
* refocusing or shifting the user's display (leaves the user in place).
*/
const valueDateParts = parseDate(value);
if (valueDateParts) {
warnIfValueOutOfBounds(valueDateParts, minParts, maxParts);
if (Array.isArray(valueDateParts)) {
this.activePartsClone = [...valueDateParts];
} else {
const { month, day, year, hour, minute } = valueDateParts;
const ampm = hour != null ? (hour >= 12 ? 'pm' : 'am') : undefined;
this.activePartsClone = {
...this.activeParts,
month,
day,
year,
hour,
minute,
ampm,
};
/**
* The working parts am/pm value must be updated when the value changes, to
* ensure the time picker hour column values are generated correctly.
*
* Note that we don't need to do this if valueDateParts is an array, since
* multiple="true" does not apply to time pickers.
*/
this.setWorkingParts({
...workingParts,
ampm,
});
}
} else {
printIonWarning(`Unable to parse date string: ${value}. Please provide a valid ISO 8601 datetime string.`);
}
this.processValue(value);
}
this.emitStyle();
@@ -596,9 +567,9 @@ export class Datetime implements ComponentInterface {
* data. This should be used when rendering an
* interface in an environment where the `value`
* may not be set. This function works
* by returning the first selected date in
* "activePartsClone" and then falling back to
* defaultParts if no active date is selected.
* by returning the first selected date and then
* falling back to defaultParts if no active date
* is selected.
*/
private getActivePartsWithFallback = () => {
const { defaultParts } = this;
@@ -606,8 +577,8 @@ export class Datetime implements ComponentInterface {
};
private getActivePart = () => {
const { activePartsClone } = this;
return Array.isArray(activePartsClone) ? activePartsClone[0] : activePartsClone;
const { activeParts } = this;
return Array.isArray(activeParts) ? activeParts[0] : activeParts;
};
private closeParentOverlay = () => {
@@ -627,7 +598,7 @@ export class Datetime implements ComponentInterface {
};
private setActiveParts = (parts: DatetimeParts, removeDate = false) => {
const { multiple, minParts, maxParts, activePartsClone } = this;
const { multiple, minParts, maxParts, activeParts } = this;
/**
* When setting the active parts, it is possible
@@ -643,16 +614,7 @@ export class Datetime implements ComponentInterface {
this.setWorkingParts(validatedParts);
if (multiple) {
/**
* We read from activePartsClone here because valueChanged() only updates that,
* so it's the more reliable source of truth. If we read from activeParts, then
* if you click July 1, manually set the value to July 2, and then click July 3,
* the new value would be [July 1, July 3], ignoring the value set.
*
* We can then pass the new value to activeParts (rather than activePartsClone)
* since the clone will be updated automatically by activePartsChanged().
*/
const activePartsArray = Array.isArray(activePartsClone) ? activePartsClone : [activePartsClone];
const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts];
if (removeDate) {
this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts));
} else {
@@ -800,7 +762,7 @@ export class Datetime implements ComponentInterface {
/**
* Get the number of padding days so
* we know how much to offset our next selector by
* to grab the correct calenday-day element.
* to grab the correct calendar-day element.
*/
const padding = currentMonth.querySelectorAll('.calendar-day-padding');
const { day } = this.workingParts;
@@ -814,7 +776,7 @@ export class Datetime implements ComponentInterface {
* and focus it.
*/
const dayEl = currentMonth.querySelector(
`.calendar-day:nth-of-type(${padding.length + day})`
`.calendar-day-wrapper:nth-of-type(${padding.length + day}) .calendar-day`
) as HTMLElement | null;
if (dayEl) {
dayEl.focus();
@@ -908,6 +870,20 @@ export class Datetime implements ComponentInterface {
const monthBox = month.getBoundingClientRect();
if (Math.abs(monthBox.x - box.x) > 2) return;
/**
* If we're force-rendering a month, assume we've
* scrolled to that and return it.
*
* If forceRenderDate is ever used in a context where the
* forced month is not immediately auto-scrolled to, this
* should be updated to also check whether `month` has the
* same month and year as the forced date.
*/
const { forceRenderDate } = this;
if (forceRenderDate !== undefined) {
return { month: forceRenderDate.month, year: forceRenderDate.year, day: forceRenderDate.day };
}
/**
* From here, we can determine if the start
* month or the end month was scrolled into view.
@@ -976,6 +952,10 @@ export class Datetime implements ComponentInterface {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
calendarBodyRef.style.removeProperty('overflow');
if (this.resolveForceDateScrolling) {
this.resolveForceDateScrolling();
}
});
};
@@ -1193,13 +1173,21 @@ export class Datetime implements ComponentInterface {
}
private processValue = (value?: string | string[] | null) => {
const hasValue = value !== null && value !== undefined;
const hasValue = value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
const { minParts, maxParts } = this;
const { minParts, maxParts, workingParts, el } = this;
this.warnIfIncorrectValueUsage();
/**
* Return early if the value wasn't parsed correctly, such as
* if an improperly formatted date string was provided.
*/
if (!valueToProcess) {
return;
}
/**
* Datetime should only warn of out of bounds values
* if set by the user. If the `value` is undefined,
@@ -1218,19 +1206,11 @@ export class Datetime implements ComponentInterface {
* that the values don't necessarily have to be in order.
*/
const singleValue = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess;
const targetValue = clampDate(singleValue, minParts, maxParts);
const { month, day, year, hour, minute } = clampDate(singleValue, minParts, maxParts);
const { month, day, year, hour, minute } = targetValue;
const ampm = parseAmPm(hour!);
this.setWorkingParts({
month,
day,
year,
hour,
minute,
ampm,
});
/**
* Since `activeParts` indicates a value that
* been explicitly selected either by the
@@ -1258,6 +1238,67 @@ export class Datetime implements ComponentInterface {
*/
this.activeParts = [];
}
/**
* Only animate if:
* 1. We're using grid style (wheel style pickers should just jump to new value)
* 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to)
* 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example)
* 4. The month/year picker is not open (since you wouldn't see the animation anyway)
*/
const didChangeMonth =
(month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year);
const bodyIsVisible = el.classList.contains('datetime-ready');
const { isGridStyle, showMonthAndYear } = this;
if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) {
this.animateToDate(targetValue);
} else {
/**
* We only need to do this if we didn't just animate to a new month,
* since that calls prevMonth/nextMonth which calls setWorkingParts for us.
*/
this.setWorkingParts({
month,
day,
year,
hour,
minute,
ampm,
});
}
};
private animateToDate = async (targetValue: DatetimeParts) => {
const { workingParts } = this;
/**
* Tell other render functions that we need to force the
* target month to appear in place of the actual next/prev month.
* Because this is a State variable, a rerender will be triggered
* automatically, updating the rendered months.
*/
this.forceRenderDate = targetValue;
/**
* Flag that we've started scrolling to the forced date.
* The resolve function will be called by the datetime's
* scroll listener when it's done updating everything.
* This is a replacement for making prev/nextMonth async,
* since the logic we're waiting on is in a listener.
*/
const forceDateScrollingPromise = new Promise<void>((resolve) => {
this.resolveForceDateScrolling = resolve;
});
/**
* Animate smoothly to the forced month. This will also update
* workingParts and correct the surrounding months for us.
*/
const targetMonthIsBefore = isBefore(targetValue, workingParts);
targetMonthIsBefore ? this.prevMonth() : this.nextMonth();
await forceDateScrollingPromise;
this.resolveForceDateScrolling = undefined;
this.forceRenderDate = undefined;
};
componentWillLoad() {
@@ -1286,16 +1327,18 @@ export class Datetime implements ComponentInterface {
}
}
this.processMinParts();
this.processMaxParts();
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
const yearValues = (this.parsedYearValues = convertToArrayOfNumbers(this.yearValues));
const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues));
const todayParts = (this.todayParts = parseDate(getToday()));
const todayParts = (this.todayParts = parseDate(getToday())!);
this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
this.processMinParts();
this.processMaxParts();
this.processValue(this.value);
this.emitStyle();
@@ -2042,7 +2085,7 @@ export class Datetime implements ComponentInterface {
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
this.locale,
referenceParts,
this.activePartsClone,
this.activeParts,
this.todayParts,
this.minParts,
this.maxParts,
@@ -2079,69 +2122,87 @@ export class Datetime implements ComponentInterface {
dateStyle = getHighlightStyles(highlightedDates, dateIsoString, el);
}
let dateParts = undefined;
// "Filler days" at the beginning of the grid should not get the calendar day
// CSS parts added to them
if (!isCalendarPadding) {
dateParts = `calendar-day${isActive ? ' active' : ''}${isToday ? ' today' : ''}${
isCalDayDisabled ? ' disabled' : ''
}`;
}
return (
<button
tabindex="-1"
data-day={day}
data-month={month}
data-year={year}
data-index={index}
data-day-of-week={dayOfWeek}
disabled={isCalDayDisabled}
class={{
'calendar-day-padding': isCalendarPadding,
'calendar-day': true,
'calendar-day-active': isActive,
'calendar-day-today': isToday,
}}
style={
dateStyle && {
color: dateStyle.textColor,
}
}
aria-hidden={isCalendarPadding ? 'true' : null}
aria-selected={ariaSelected}
aria-label={ariaLabel}
onClick={() => {
if (isCalendarPadding) {
return;
}
<div class="calendar-day-wrapper">
<button
// We need to use !important for the inline styles here because
// otherwise the CSS shadow parts will override these styles.
// See https://github.com/WICG/webcomponents/issues/847
// Both the CSS shadow parts and highlightedDates styles are
// provided by the developer, but highlightedDates styles should
// always take priority.
ref={(el) => {
if (el) {
el.style.setProperty('color', `${dateStyle ? dateStyle.textColor : ''}`, 'important');
el.style.setProperty(
'background-color',
`${dateStyle ? dateStyle.backgroundColor : ''}`,
'important'
);
}
}}
tabindex="-1"
data-day={day}
data-month={month}
data-year={year}
data-index={index}
data-day-of-week={dayOfWeek}
disabled={isCalDayDisabled}
class={{
'calendar-day-padding': isCalendarPadding,
'calendar-day': true,
'calendar-day-active': isActive,
'calendar-day-today': isToday,
}}
part={dateParts}
aria-hidden={isCalendarPadding ? 'true' : null}
aria-selected={ariaSelected}
aria-label={ariaLabel}
onClick={() => {
if (isCalendarPadding) {
return;
}
this.setWorkingParts({
...this.workingParts,
month,
day,
year,
});
// multiple only needs date info, so we can wipe out other fields like time
if (multiple) {
this.setActiveParts(
{
month,
day,
year,
},
isActive
);
} else {
this.setActiveParts({
...activePart,
this.setWorkingParts({
...this.workingParts,
month,
day,
year,
});
}
}}
>
<div
class="calendar-day-highlight"
style={{
backgroundColor: dateStyle?.backgroundColor,
// multiple only needs date info, so we can wipe out other fields like time
if (multiple) {
this.setActiveParts(
{
month,
day,
year,
},
isActive
);
} else {
this.setActiveParts({
...activePart,
month,
day,
year,
});
}
}}
></div>
{text}
</button>
>
{text}
</button>
</div>
);
})}
</div>
@@ -2151,7 +2212,7 @@ export class Datetime implements ComponentInterface {
private renderCalendarBody() {
return (
<div class="calendar-body ion-focusable" ref={(el) => (this.calendarBodyRef = el)} tabindex="0">
{generateMonths(this.workingParts).map(({ month, year }) => {
{generateMonths(this.workingParts, this.forceRenderDate).map(({ month, year }) => {
return this.renderMonth(month, year);
})}
</div>
@@ -2360,7 +2421,19 @@ export class Datetime implements ComponentInterface {
}
render() {
const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size } = this;
const {
name,
value,
disabled,
el,
color,
readonly,
showMonthAndYear,
preferWheel,
presentation,
size,
isGridStyle,
} = this;
const mode = getIonMode(this);
const isMonthAndYearPresentation =
presentation === 'year' || presentation === 'month' || presentation === 'month-year';
@@ -2368,7 +2441,6 @@ export class Datetime implements ComponentInterface {
const monthYearPickerOpen = showMonthAndYear && !isMonthAndYearPresentation;
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
const hasWheelVariant = hasDatePresentation && preferWheel;
const hasGrid = hasDatePresentation && !preferWheel;
renderHiddenInput(true, el, name, formatValue(value), disabled);
@@ -2387,7 +2459,7 @@ export class Datetime implements ComponentInterface {
[`datetime-presentation-${presentation}`]: true,
[`datetime-size-${size}`]: true,
[`datetime-prefer-wheel`]: hasWheelVariant,
[`datetime-grid`]: hasGrid,
[`datetime-grid`]: isGridStyle,
}),
}}
>

View File

@@ -490,3 +490,40 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
});
});
});
/**
* This behavior does not differ across
* directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('datetime: focus'), () => {
test('should focus the selected day and then the day after', async ({ page }) => {
await page.setContent(
`
<ion-datetime value="2023-08-01"></ion-datetime>
`,
config
);
await page.waitForSelector('.datetime-ready');
const datetime = page.locator('ion-datetime');
const day = datetime.locator(`.calendar-day[data-day='1'][data-month='8']`);
await day.focus();
await page.waitForChanges();
await expect(day).toBeFocused();
await expect(datetime).toHaveScreenshot(screenshot(`datetime-focus-selected-calendar-day`));
await page.keyboard.press('ArrowRight');
await page.waitForChanges();
const nextDay = datetime.locator(`.calendar-day[data-day='2'][data-month='8']`);
await expect(nextDay).toBeFocused();
await expect(datetime).toHaveScreenshot(screenshot(`datetime-focus-calendar-day`));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -9,7 +9,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/datetime/test/color', config);
const datetime = page.locator('ion-datetime');
const datetime = page.locator('#color-datetime');
await expect(datetime).toHaveScreenshot(screenshot(`datetime-color`));

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -9,11 +9,18 @@
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-checkbox,
ion-select {
margin-inline-start: 10px;
margin-inline-end: 10px;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, 250px);
grid-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
@@ -29,10 +36,14 @@
border-radius: 8px;
}
#color-name::first-letter {
text-transform: capitalize;
}
/*
* Dark Colors
* -------------------------------------------
*/
* Dark Theme
* -------------------------------------------
*/
body.dark {
--ion-color-primary: #428cff;
@@ -100,9 +111,9 @@
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
* iOS Dark Theme
* -------------------------------------------
*/
.ios body.dark {
--ion-background-color: #000000;
@@ -144,9 +155,9 @@
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
* Material Design Dark Theme
* -------------------------------------------
*/
.md body.dark {
--ion-background-color: #121212;
@@ -191,30 +202,23 @@
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Color</ion-title>
<ion-item lines="none" slot="end">
<ion-label>Dark Mode</ion-label>
<ion-checkbox slot="end"></ion-checkbox>
</ion-item>
<ion-item lines="none" slot="end">
<ion-label>Color</ion-label>
<ion-select value="danger">
<ion-select-option value="primary">Primary</ion-select-option>
<ion-select-option value="secondary">Secondary</ion-select-option>
<ion-select-option value="tertiary">Tertiary</ion-select-option>
<ion-select-option value="success">Success</ion-select-option>
<ion-select-option value="warning">Warning</ion-select-option>
<ion-select-option value="danger">Danger</ion-select-option>
</ion-select>
</ion-item>
<ion-checkbox>Dark Mode</ion-checkbox>
<ion-select label="Color" justify="end" slot="end" value="danger">
<ion-select-option value="primary">Primary</ion-select-option>
<ion-select-option value="secondary">Secondary</ion-select-option>
<ion-select-option value="tertiary">Tertiary</ion-select-option>
<ion-select-option value="success">Success</ion-select-option>
<ion-select-option value="warning">Warning</ion-select-option>
<ion-select-option value="danger">Danger</ion-select-option>
</ion-select>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<h2 id="color-name">Danger</h2>
<ion-datetime
id="color-datetime"
color="danger"
value="2022-05-03"
show-default-title="true"
@@ -224,14 +228,14 @@
</div>
</ion-content>
<script>
const colorDatetime = document.querySelector('#color-datetime');
const colorName = document.querySelector('#color-name');
const colorSelect = document.querySelector('ion-select');
const darkModeToggle = document.querySelector('ion-checkbox');
const datetimes = document.querySelectorAll('ion-datetime');
colorSelect.addEventListener('ionChange', (ev) => {
datetimes.forEach((datetime) => {
datetime.color = ev.detail.value;
});
colorDatetime.color = ev.detail.value;
colorName.innerHTML = ev.detail.value;
});
darkModeToggle.addEventListener('ionChange', (ev) => {

View File

@@ -14,7 +14,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
test('should allow styling time picker in grid style datetimes', async ({ page }) => {
const timeButton = page.locator('ion-datetime .time-body');
const timeButton = page.locator('#custom-grid .time-body');
const popover = page.locator('.popover-viewport');
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
@@ -26,5 +26,41 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await expect(popover).toHaveScreenshot(screenshot(`datetime-custom-time-picker`));
await expect(timeButton).toHaveScreenshot(screenshot(`datetime-custom-time-button-active`));
});
test('should allow styling calendar days in grid style datetimes', async ({ page }) => {
const datetime = page.locator('#custom-calendar-days');
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-calendar-days`));
});
});
});
/**
* This behavior does not differ across
* directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('datetime: custom focus'), () => {
test('should focus the selected day and then the day after', async ({ page }) => {
await page.goto(`/src/components/datetime/test/custom`, config);
const datetime = page.locator('#custom-calendar-days');
const day = datetime.locator(`.calendar-day[data-day='15'][data-month='6']`);
await day.focus();
await page.waitForChanges();
await expect(day).toBeFocused();
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-focus-selected-calendar-day`));
await page.keyboard.press('ArrowRight');
await page.waitForChanges();
const nextDay = datetime.locator(`.calendar-day[data-day='16'][data-month='6']`);
await expect(nextDay).toBeFocused();
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-focus-calendar-day`));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Some files were not shown because too many files have changed in this diff Show More